Esempio n. 1
0
    def set_range(self, start_index=0, end_index=None):
        '''
        Set range of tests to run

        .. deprecated::
            use :func:`~kitty.fuzzers.base.BaseFuzzer.set_test_list`

        :param start_index: index to start at (default=0)
        :param end_index: index to end at(default=None)
        '''
        if end_index is not None:
            end_index += 1
        self._test_list = StartEndList(start_index, end_index)
        self.session_info.start_index = start_index
        self.session_info.current_index = 0
        self.session_info.end_index = end_index
        self.session_info.test_list_str = self._test_list.as_test_list_str()
        return self
Esempio n. 2
0
    def set_test_list(self, test_list_str=''):
        '''
        :param test_list_str: listing of the test to execute

        The test list should be a comma-delimited string, and each element
        should be one of the following forms:

        '-x' - run from test 0 to test x
        'x-' - run from test x to the end
        'x' - run test x
        'x-y' - run from test x to test y

        To execute all tests, pass None or an empty string
        '''
        self.session_info.test_list_str = test_list_str
        self._test_list = RangesList(test_list_str)
Esempio n. 3
0
    def set_test_list(self, test_list_str=''):
        '''
        :param test_list_str: listing of the test to execute

        The test list should be a comma-delimited string, and each element
        should be one of the following forms:

        '-x' - run from test 0 to test x
        'x-' - run from test x to the end
        'x' - run test x
        'x-y' - run from test x to test y

        To execute all tests, pass None or an empty string
        '''
        self.session_info.test_list_str = test_list_str
        self._test_list = RangesList(test_list_str)
Esempio n. 4
0
    def set_range(self, start_index=0, end_index=None):
        '''
        Set range of tests to run

        .. deprecated::
            use :func:`~kitty.fuzzers.base.BaseFuzzer.set_test_list`

        :param start_index: index to start at (default=0)
        :param end_index: index to end at(default=None)
        '''
        if end_index is not None:
            end_index += 1
        self._test_list = StartEndList(start_index, end_index)
        self.session_info.start_index = start_index
        self.session_info.current_index = 0
        self.session_info.end_index = end_index
        self.session_info.test_list_str = self._test_list.as_test_list_str()
        return self
Esempio n. 5
0
class BaseFuzzer(KittyObject):
    '''
    Common members and logic for client and server fuzzers.
    This class should not be instantiated, only subclassed.
    '''
    def __init__(self, name='', logger=None, option_line=None):
        '''
        :param name: name of the object
        :param logger: logger for the object (default: None)
        :param option_line: cmd line options to the fuzzer (dafult: None)
        '''
        super(BaseFuzzer, self).__init__(name, logger)
        # session to fuzz
        self.model = None
        self.dataman = None
        self.session_info = SessionInfo()
        self.config = _Configuration(
            delay_secs=0,
            store_all_reports=False,
            session_file_name=None,
            max_failures=None,
        )
        # user interface
        self.user_interface = None
        # target
        self.target = None
        # event to implement pause / continue
        self._continue_event = Event()
        self._continue_event.set()
        self._fuzz_path = None
        self._fuzz_node = None
        self._last_payload = None
        self._skip_env_test = False
        self._in_environment_test = True
        self._started = False
        self._test_list = None
        self._handle_options(option_line)

    def _next_mutation(self):
        '''
        :return: True if mutated, False otherwise
        '''
        if self._keep_running():
            current_idx = self.model.current_index()
            self.session_info.current_index = current_idx
            next_idx = self._test_list.current()
            if next_idx is None:
                return False
            skip = next_idx - current_idx - 1
            if skip > 0:
                self.model.skip(skip)
            self._test_list.next()
            resp = self.model.mutate()
            return resp
        return False

    def _handle_options(self, option_line):
        '''
        Handle options from command line, in docopt style.
        This allows passing arguments to the fuzzer from the command line
        without the need to re-write it in each runner.

        :param option_line: string with the command line options to be parsed.
        '''
        if option_line is not None:
            usage = '''
            These are the options to the kitty fuzzer object, not the options to the runner.

            Usage:
                fuzzer [options] [-v ...]

            Options:
                -d --delay <delay>              delay between tests in secodes, float number
                -f --session <session-file>     session file name to use
                -n --no-env-test                don't perform environment test before the fuzzing session
                -r --retest <session-file>      retest failed/error tests from a session file
                -t --test-list <test-list>      a comma delimited test list string of the form "-10,12,15-20,30-"
                -v --verbose                    be more verbose in the log

            Removed options:
                end, start - use --test-list instead
            '''
            options = docopt.docopt(usage, shlex.split(option_line))

            # ranges
            if options['--retest']:
                retest_file = options['--retest']
                try:
                    test_list_str = self._get_test_list_from_session_file(
                        retest_file)
                except Exception as ex:
                    raise KittyException(
                        'Failed to open session file (%s) for retesting: %s' %
                        (retest_file, ex))
            else:
                test_list_str = options['--test-list']
            self._set_test_ranges(None, None, test_list_str)

            # session file
            session_file = options['--session']
            if session_file is not None:
                self.set_session_file(session_file)

            # delay between tests
            delay = options['--delay']
            if delay is not None:
                self.set_delay_between_tests(float(delay))

            # environment test
            skip_env_test = options['--no-env-test']
            if skip_env_test:
                self.set_skip_env_test(True)

            # verbosity
            verbosity = options['--verbose']
            self.set_verbosity(verbosity)

    def _get_test_list_from_session_file(self, session_file):
        dm = DataManager(session_file)
        dm.start()
        test_ids = dm.get_report_test_ids()
        if len(test_ids) == 0:
            raise KittyException('No failed tests in the session file %s' %
                                 session_file)
        test_list_str = ','.join('%s' % i for i in test_ids)
        dm.stop()
        return test_list_str

    def _set_test_ranges(self, start, end, test_list_str):
        if test_list_str and test_list_str.strip():
            self.set_test_list(test_list_str)
        else:
            s = 0 if start is None else int(start)
            e = end if end is None else int(end)
            self.set_range(s, e)

    def set_skip_env_test(self, skip_env_test=True):
        '''
        Set whether to skip the environment test.
        Call this if the environment test cannot pass
        and you prefer to start the tests without it.

        :param skip_env_test: skip the environment test (default: True)
        '''
        self._skip_env_test = skip_env_test

    def set_delay_duration(self, delay_duration):
        '''
        .. deprecated::
            use :func:`~kitty.fuzzers.base.BaseFuzzer.set_delay_between_tests`
        '''
        raise DeprecationWarning(
            'API was changed, use set_delay_between_tests')

    def set_delay_between_tests(self, delay_secs):
        '''
        Set duration between tests

        :param delay_secs: delay between tests (in seconds)
        '''
        self.config.delay_secs = delay_secs
        return self

    def set_store_all_reports(self, store_all_reports):
        '''
        :param store_all_reports: should all reports be stored
        '''
        self.config.store_all_reports = store_all_reports
        return self

    def set_session_file(self, filename):
        '''
        Set session file name, to keep state between runs

        :param filename: session file name
        '''
        self.config.session_file_name = filename
        return self

    def set_model(self, model):
        '''
        Set the model to fuzz

        :type model: :class:`~kitty.model.high_level.base.BaseModel` or a subclass
        :param model: Model object to fuzz
        '''
        self.model = model
        if self.model:
            self.model.set_notification_handler(self)
            self.handle_stage_changed(model)
        return self

    def set_target(self, target):
        '''
        :param target: target object
        '''
        self.target = target
        if target:
            self.target.set_fuzzer(self)
        return self

    def set_max_failures(self, max_failures):
        '''
        :param max_failures: maximum failures before stopping the fuzzing session
        '''
        self.config.max_failures = max_failures
        return self

    def set_range(self, start_index=0, end_index=None):
        '''
        Set range of tests to run

        .. deprecated::
            use :func:`~kitty.fuzzers.base.BaseFuzzer.set_test_list`

        :param start_index: index to start at (default=0)
        :param end_index: index to end at(default=None)
        '''
        if end_index is not None:
            end_index += 1
        self._test_list = StartEndList(start_index, end_index)
        self.session_info.start_index = start_index
        self.session_info.current_index = 0
        self.session_info.end_index = end_index
        self.session_info.test_list_str = self._test_list.as_test_list_str()
        return self

    def set_test_list(self, test_list_str=''):
        '''
        :param test_list_str: listing of the test to execute

        The test list should be a comma-delimited string, and each element
        should be one of the following forms:

        '-x' - run from test 0 to test x
        'x-' - run from test x to the end
        'x' - run test x
        'x-y' - run from test x to test y

        To execute all tests, pass None or an empty string
        '''
        self.session_info.test_list_str = test_list_str
        self._test_list = RangesList(test_list_str)

    def set_interface(self, interface):
        '''
        :param interface: user interface
        '''
        self.user_interface = interface
        return self

    def _check_session_validity(self):
        current_version = _get_current_version()
        if current_version != self.session_info.kitty_version:
            raise KittyException(
                'kitty version in stored session (%s) != current kitty version (%s)'
                % (current_version, self.session_info.kitty_version))
        model_hash = self.model.hash()
        if model_hash != self.session_info.data_model_hash:
            raise KittyException(
                'data model hash in stored session(%s) != current data model hash (%s)'
                % (model_hash, self.session_info.data_model_hash))

    def start(self):
        '''
        Start the fuzzing session

        If fuzzer already running, it will return immediatly
        '''
        if self._started:
            self.logger.warning('called while fuzzer is running. ignoring.')
            return
        self._started = True
        assert (self.model)
        assert (self.user_interface)
        assert (self.target)

        if self._load_session():
            self._check_session_validity()
            self._set_test_ranges(self.session_info.start_index,
                                  self.session_info.end_index,
                                  self.session_info.test_list_str)
        else:
            self.session_info.kitty_version = _get_current_version()
            # TODO: write hash for high level
            self.session_info.data_model_hash = self.model.hash()
        # if self.session_info.end_index is None:
        #     self.session_info.end_index = self.model.last_index()
        if self._test_list is None:
            self._test_list = StartEndList(0, self.model.num_mutations())
        else:
            self._test_list.set_last(self.model.last_index())

        list_count = self._test_list.get_count()
        self._test_list.skip(list_count - 1)
        self.session_info.end_index = self._test_list.current()
        self._test_list.reset()
        self._store_session()
        self._test_list.skip(self.session_info.current_index)
        self.session_info.test_list_str = self._test_list.as_test_list_str()

        self._set_signal_handler()
        self.user_interface.set_data_provider(self.dataman)
        self.user_interface.set_continue_event(self._continue_event)
        self.user_interface.start()

        self.session_info.start_time = time.time()
        try:
            self._start_message()
            self.target.setup()
            start_from = self.session_info.current_index
            if self._skip_env_test:
                self.logger.info('Skipping environment test')
            else:
                self.logger.info('Performing environment test')
                self._test_environment()
            self._in_environment_test = False
            self._test_list.reset()
            self._test_list.skip(start_from)
            self.session_info.current_index = start_from
            self.model.skip(self._test_list.current())
            self._start()
            return True
        except Exception as e:
            self.logger.error('Error occurred while fuzzing: %s', repr(e))
            self.logger.error(traceback.format_exc())
            return False

    def handle_stage_changed(self, model):
        '''
        handle a stage change in the data model

        :param model: the data model that was changed
        '''
        stages = model.get_stages()
        if self.dataman:
            self.dataman.set('stages', stages)

    def _test_environment(self):
        '''
        Test that the environment is ready to run.
        Should be implemented by subclass
        '''
        raise NotImplementedError('should be implemented by subclass')

    def _start(self):
        self.not_implemented('_start')

    def _update_test_info(self):
        test_info = self.model.get_test_info()
        self.dataman.set('test_info', test_info)
        template_info = self.model.get_template_info()
        self.dataman.set('template_info', template_info)

    def _pre_test(self):
        self._update_test_info()
        self.session_info.current_index = self._test_list.current()
        self.target.pre_test(self.model.current_index())

    def _post_test(self):
        '''
        :return: True if test failed
        '''
        failure_detected = False
        self.target.post_test(self.model.current_index())
        report = self._get_report()
        status = report.get_status()
        if self._in_environment_test:
            return status != Report.PASSED
        if status != Report.PASSED:
            self._store_report(report)
            self.user_interface.failure_detected()
            failure_detected = True
            self.logger.warning('!! Failure detected !!')
        elif self.config.store_all_reports:
            self._store_report(report)
        if failure_detected:
            self.session_info.failure_count += 1
        self._store_session()
        if self.config.delay_secs:
            self.logger.debug('delaying for %f seconds',
                              self.config.delay_secs)
            time.sleep(self.config.delay_secs)
        return failure_detected

    def _get_report(self):
        report = self.target.get_report()
        return report

    def _start_message(self):
        self.logger.info(
            '''
                 --------------------------------------------------
                 Starting fuzzing session
                 Target: %s
                 UI: %s
                 Log: %s

                 Total possible mutation count: %d
                 --------------------------------------------------
                                 Happy hacking
                 --------------------------------------------------
            ''',
            self.target.get_description(),
            self.user_interface.get_description(),
            self.get_log_file_name(),
            self.model.num_mutations(),
        )

    def _end_message(self):
        tested = self._test_list.get_progress()
        self.logger.info(
            '''
                         --------------------------------------------------
                         Finished fuzzing session
                         Target: %s

                         Tested %d mutation%s
                         Failure count: %d
                         --------------------------------------------------
            ''', self.target.get_description(), tested,
            's' if tested > 1 else '', self.session_info.failure_count)

    def _test_info(self):
        fuzz_node_info = self.model.get_test_info()
        self.logger.info('Current test: %s' % self.model.current_index())
        self.logger.debug('----------------------------------------------')
        keys = sorted(fuzz_node_info.keys())
        keys = [k for k in keys if k.startswith('node/field')]
        keys = [k for k in keys if not isinstance(fuzz_node_info[k], bool)]
        key_max_len = 0
        for key in keys:
            if len(key) > key_max_len:
                key_max_len = len(str(key))
        for k in keys:
            v = str(fuzz_node_info[k])
            k = str(k)
            pad = ' ' * (key_max_len - len(k) + 1)
            if len(v) > 70:
                v = v[:70] + '...'
            self.logger.debug('%s:%s%s' % (k, pad, v))
        self.logger.debug('----------------------------------------------')

    def _check_pause(self):
        if not self._continue_event.is_set():
            self.logger.info(
                'fuzzer paused, waiting for resume command from user')
            self._continue_event.wait()
            self.logger.info('resume command received, continue running')

    def stop(self):
        '''
        stop the fuzzing session
        '''
        assert (self.model)
        assert (self.user_interface)
        assert (self.target)
        self.user_interface.stop()
        self.target.teardown()
        self.dataman.submit_task(None)
        self._un_set_signal_handler()

    def _store_report(self, report):
        self.logger.debug('<in>')
        report.add('test_number', self.model.current_index())
        report.add('fuzz_path', self.model.get_sequence_str())
        test_info = self.model.get_test_info()
        data_model_report = Report(name='Data Model')
        for k, v in test_info.items():
            new_entries = _flatten_dict_entry(k, v)
            for (k_, v_) in new_entries:
                data_model_report.add(k_, v_)
        report.add(data_model_report.get_name(), data_model_report)
        payload = self._last_payload
        if payload is not None:
            data_report = Report('payload')
            data_report.add('raw', payload)
            data_report.add('hex', hexlify(payload).decode())
            data_report.add('length', len(payload))
            report.add('payload', data_report)
        else:
            report.add('payload', None)

        self.dataman.store_report(report, self.model.current_index())
        self.dataman.get_report_by_id(self.model.current_index())

    def _store_session(self):
        self._set_session_info()

    def _get_session_info(self):
        info = self.dataman.get_session_info()
        return info

    def _get_test_info(self):
        info = self.dataman.get('test_info')
        return info

    def _set_session_info(self):
        self.dataman.set_session_info(self.session_info)
        self.dataman.set('fuzzer_name', self.get_name())
        self.dataman.set('session_file_name', self.config.session_file_name)

    def _load_session(self):
        if not self.config.session_file_name:
            self.config.session_file_name = ':memory:'
        self.dataman = DataManager(self.config.session_file_name)
        self.dataman.start()
        if self.model:
            self.handle_stage_changed(self.model)
        self.dataman.set('log_file_name', self.get_log_file_name())
        info = self._get_session_info()
        if info:
            self.logger.info('Loaded session from DB')
            self.session_info = info
            return True
        self.logger.info('No session loaded')
        self._set_session_info()
        return False

    def _exit_now(self, dummy1, dummy2):
        self.stop()
        sys.exit(0)

    def _keep_running(self):
        '''
        Should we still fuzz??
        '''
        if self.config.max_failures:
            if self.session_info.failure_count >= self.config.max_failures:
                return False
        return self._test_list.current() is not None

    def _set_signal_handler(self):
        '''
        Replace the signal handler with self._exit_now
        '''
        import signal
        signal.signal(signal.SIGINT, self._exit_now)

    @classmethod
    def _un_set_signal_handler(cls):
        '''
        Set the default signal handler
        '''
        import signal
        signal.signal(signal.SIGINT, signal.SIG_DFL)
Esempio n. 6
0
    def start(self):
        '''
        Start the fuzzing session

        If fuzzer already running, it will return immediatly
        '''
        if self._started:
            self.logger.warning('called while fuzzer is running. ignoring.')
            return
        self._started = True
        assert (self.model)
        assert (self.user_interface)
        assert (self.target)

        if self._load_session():
            self._check_session_validity()
            self._set_test_ranges(self.session_info.start_index,
                                  self.session_info.end_index,
                                  self.session_info.test_list_str)
        else:
            self.session_info.kitty_version = _get_current_version()
            # TODO: write hash for high level
            self.session_info.data_model_hash = self.model.hash()
        # if self.session_info.end_index is None:
        #     self.session_info.end_index = self.model.last_index()
        if self._test_list is None:
            self._test_list = StartEndList(0, self.model.num_mutations())
        else:
            self._test_list.set_last(self.model.last_index())

        list_count = self._test_list.get_count()
        self._test_list.skip(list_count - 1)
        self.session_info.end_index = self._test_list.current()
        self._test_list.reset()
        self._store_session()
        self._test_list.skip(self.session_info.current_index)
        self.session_info.test_list_str = self._test_list.as_test_list_str()

        self._set_signal_handler()
        self.user_interface.set_data_provider(self.dataman)
        self.user_interface.set_continue_event(self._continue_event)
        self.user_interface.start()

        self.session_info.start_time = time.time()
        try:
            self._start_message()
            self.target.setup()
            start_from = self.session_info.current_index
            if self._skip_env_test:
                self.logger.info('Skipping environment test')
            else:
                self.logger.info('Performing environment test')
                self._test_environment()
            self._in_environment_test = False
            self._test_list.reset()
            self._test_list.skip(start_from)
            self.session_info.current_index = start_from
            self.model.skip(self._test_list.current())
            self._start()
            return True
        except Exception as e:
            self.logger.error('Error occurred while fuzzing: %s', repr(e))
            self.logger.error(traceback.format_exc())
            return False
Esempio n. 7
0
class BaseFuzzer(KittyObject):
    '''
    Common members and logic for client and server fuzzers.
    This class should not be instantiated, only subclassed.
    '''

    def __init__(self, name='', logger=None, option_line=None):
        '''
        :param name: name of the object
        :param logger: logger for the object (default: None)
        :param option_line: cmd line options to the fuzzer (dafult: None)
        '''
        super(BaseFuzzer, self).__init__(name, logger)
        # session to fuzz
        self.model = None
        self.dataman = None
        self.session_info = SessionInfo()
        self.config = _Configuration(
            delay_secs=0,
            store_all_reports=False,
            session_file_name=None,
            max_failures=None,
        )
        # user interface
        self.user_interface = None
        # target
        self.target = None
        # event to implement pause / continue
        self._continue_event = Event()
        self._continue_event.set()
        self._fuzz_path = None
        self._fuzz_node = None
        self._last_payload = None
        self._skip_env_test = False
        self._in_environment_test = True
        self._started = False
        self._test_list = None
        self._handle_options(option_line)

    def _next_mutation(self):
        '''
        :return: True if mutated, False otherwise
        '''
        if self._keep_running():
            current_idx = self.model.current_index()
            self.session_info.current_index = current_idx
            next_idx = self._test_list.current()
            if next_idx is None:
                return False
            skip = next_idx - current_idx - 1
            if skip > 0:
                self.model.skip(skip)
            self._test_list.next()
            resp = self.model.mutate()
            return resp
        return False

    def _handle_options(self, option_line):
        '''
        Handle options from command line, in docopt style.
        This allows passing arguments to the fuzzer from the command line
        without the need to re-write it in each runner.

        :param option_line: string with the command line options to be parsed.
        '''
        if option_line is not None:
            usage = '''
            These are the options to the kitty fuzzer object, not the options to the runner.

            Usage:
                fuzzer [options] [-v ...]

            Options:
                -d --delay <delay>              delay between tests in secodes, float number
                -f --session <session-file>     session file name to use
                -n --no-env-test                don't perform environment test before the fuzzing session
                -r --retest <session-file>      retest failed/error tests from a session file
                -t --test-list <test-list>      a comma delimited test list string of the form "-10,12,15-20,30-"
                -v --verbose                    be more verbose in the log

            Removed options:
                end, start - use --test-list instead
            '''
            options = docopt.docopt(usage, shlex.split(option_line))

            # ranges
            if options['--retest']:
                retest_file = options['--retest']
                try:
                    test_list_str = self._get_test_list_from_session_file(retest_file)
                except Exception as ex:
                    raise KittyException('Failed to open session file (%s) for retesting: %s' % (retest_file, ex))
            else:
                test_list_str = options['--test-list']
            self._set_test_ranges(None, None, test_list_str)

            # session file
            session_file = options['--session']
            if session_file is not None:
                self.set_session_file(session_file)

            # delay between tests
            delay = options['--delay']
            if delay is not None:
                self.set_delay_between_tests(float(delay))

            # environment test
            skip_env_test = options['--no-env-test']
            if skip_env_test:
                self.set_skip_env_test(True)

            # verbosity
            verbosity = options['--verbose']
            self.set_verbosity(verbosity)

    def _get_test_list_from_session_file(self, session_file):
        dm = DataManager(session_file)
        dm.start()
        test_ids = dm.get_report_test_ids()
        if len(test_ids) == 0:
            raise KittyException('No failed tests in the session file %s' % session_file)
        test_list_str = ','.join('%s' % i for i in test_ids)
        dm.stop()
        return test_list_str

    def _set_test_ranges(self, start, end, test_list_str):
        if test_list_str and test_list_str.strip():
            self.set_test_list(test_list_str)
        else:
            s = 0 if start is None else int(start)
            e = end if end is None else int(end)
            self.set_range(s, e)

    def set_skip_env_test(self, skip_env_test=True):
        '''
        Set whether to skip the environment test.
        Call this if the environment test cannot pass
        and you prefer to start the tests without it.

        :param skip_env_test: skip the environment test (default: True)
        '''
        self._skip_env_test = skip_env_test

    def set_delay_duration(self, delay_duration):
        '''
        .. deprecated::
            use :func:`~kitty.fuzzers.base.BaseFuzzer.set_delay_between_tests`
        '''
        raise DeprecationWarning('API was changed, use set_delay_between_tests')

    def set_delay_between_tests(self, delay_secs):
        '''
        Set duration between tests

        :param delay_secs: delay between tests (in seconds)
        '''
        self.config.delay_secs = delay_secs
        return self

    def set_store_all_reports(self, store_all_reports):
        '''
        :param store_all_reports: should all reports be stored
        '''
        self.config.store_all_reports = store_all_reports
        return self

    def set_session_file(self, filename):
        '''
        Set session file name, to keep state between runs

        :param filename: session file name
        '''
        self.config.session_file_name = filename
        return self

    def set_model(self, model):
        '''
        Set the model to fuzz

        :type model: :class:`~kitty.model.high_level.base.BaseModel` or a subclass
        :param model: Model object to fuzz
        '''
        self.model = model
        if self.model:
            self.model.set_notification_handler(self)
            self.handle_stage_changed(model)
        return self

    def set_target(self, target):
        '''
        :param target: target object
        '''
        self.target = target
        if target:
            self.target.set_fuzzer(self)
        return self

    def set_max_failures(self, max_failures):
        '''
        :param max_failures: maximum failures before stopping the fuzzing session
        '''
        self.config.max_failures = max_failures
        return self

    def set_range(self, start_index=0, end_index=None):
        '''
        Set range of tests to run

        .. deprecated::
            use :func:`~kitty.fuzzers.base.BaseFuzzer.set_test_list`

        :param start_index: index to start at (default=0)
        :param end_index: index to end at(default=None)
        '''
        if end_index is not None:
            end_index += 1
        self._test_list = StartEndList(start_index, end_index)
        self.session_info.start_index = start_index
        self.session_info.current_index = 0
        self.session_info.end_index = end_index
        self.session_info.test_list_str = self._test_list.as_test_list_str()
        return self

    def set_test_list(self, test_list_str=''):
        '''
        :param test_list_str: listing of the test to execute

        The test list should be a comma-delimited string, and each element
        should be one of the following forms:

        '-x' - run from test 0 to test x
        'x-' - run from test x to the end
        'x' - run test x
        'x-y' - run from test x to test y

        To execute all tests, pass None or an empty string
        '''
        self.session_info.test_list_str = test_list_str
        self._test_list = RangesList(test_list_str)

    def set_interface(self, interface):
        '''
        :param interface: user interface
        '''
        self.user_interface = interface
        return self

    def _check_session_validity(self):
        current_version = _get_current_version()
        if current_version != self.session_info.kitty_version:
            raise KittyException('kitty version in stored session (%s) != current kitty version (%s)' % (
                current_version,
                self.session_info.kitty_version))
        model_hash = self.model.hash()
        if model_hash != self.session_info.data_model_hash:
            raise KittyException('data model hash in stored session(%s) != current data model hash (%s)' % (
                model_hash,
                self.session_info.data_model_hash
            ))

    def start(self):
        '''
        Start the fuzzing session

        If fuzzer already running, it will return immediatly
        '''
        if self._started:
            self.logger.warning('called while fuzzer is running. ignoring.')
            return
        self._started = True
        assert(self.model)
        assert(self.user_interface)
        assert(self.target)

        if self._load_session():
            self._check_session_validity()
            self._set_test_ranges(
                self.session_info.start_index,
                self.session_info.end_index,
                self.session_info.test_list_str
            )
        else:
            self.session_info.kitty_version = _get_current_version()
            # TODO: write hash for high level
            self.session_info.data_model_hash = self.model.hash()
        # if self.session_info.end_index is None:
        #     self.session_info.end_index = self.model.last_index()
        if self._test_list is None:
            self._test_list = StartEndList(0, self.model.num_mutations())
        else:
            self._test_list.set_last(self.model.last_index())

        list_count = self._test_list.get_count()
        self._test_list.skip(list_count - 1)
        self.session_info.end_index = self._test_list.current()
        self._test_list.reset()
        self._store_session()
        self._test_list.skip(self.session_info.current_index)
        self.session_info.test_list_str = self._test_list.as_test_list_str()

        self._set_signal_handler()
        self.user_interface.set_data_provider(self.dataman)
        self.user_interface.set_continue_event(self._continue_event)
        self.user_interface.start()

        self.session_info.start_time = time.time()
        try:
            self._start_message()
            self.target.setup()
            start_from = self.session_info.current_index
            if self._skip_env_test:
                self.logger.info('Skipping environment test')
            else:
                self.logger.info('Performing environment test')
                self._test_environment()
            self._in_environment_test = False
            self._test_list.reset()
            self._test_list.skip(start_from)
            self.session_info.current_index = start_from
            self.model.skip(self._test_list.current())
            self._start()
            return True
        except Exception as e:
            self.logger.error('Error occurred while fuzzing: %s', repr(e))
            self.logger.error(traceback.format_exc())
            return False

    def handle_stage_changed(self, model):
        '''
        handle a stage change in the data model

        :param model: the data model that was changed
        '''
        stages = model.get_stages()
        if self.dataman:
            self.dataman.set('stages', stages)

    def _test_environment(self):
        '''
        Test that the environment is ready to run.
        Should be implemented by subclass
        '''
        raise NotImplementedError('should be implemented by subclass')

    def _start(self):
        self.not_implemented('_start')

    def _update_test_info(self):
        test_info = self.model.get_test_info()
        self.dataman.set('test_info', test_info)
        template_info = self.model.get_template_info()
        self.dataman.set('template_info', template_info)

    def _pre_test(self):
        self._update_test_info()
        self.session_info.current_index = self._test_list.current()
        self.target.pre_test(self.model.current_index())

    def _post_test(self):
        '''
        :return: True if test failed
        '''
        failure_detected = False
        self.target.post_test(self.model.current_index())
        report = self._get_report()
        status = report.get_status()
        if self._in_environment_test:
            return status != Report.PASSED
        if status != Report.PASSED:
            self._store_report(report)
            self.user_interface.failure_detected()
            failure_detected = True
            self.logger.warning('!! Failure detected !!')
        elif self.config.store_all_reports:
            self._store_report(report)
        if failure_detected:
            self.session_info.failure_count += 1
        self._store_session()
        if self.config.delay_secs:
            self.logger.debug('delaying for %f seconds', self.config.delay_secs)
            time.sleep(self.config.delay_secs)
        return failure_detected

    def _get_report(self):
        report = self.target.get_report()
        return report

    def _start_message(self):
        self.logger.info(
            '''
                 --------------------------------------------------
                 Starting fuzzing session
                 Target: %s
                 UI: %s
                 Log: %s

                 Total possible mutation count: %d
                 --------------------------------------------------
                                 Happy hacking
                 --------------------------------------------------
            ''',
            self.target.get_description(),
            self.user_interface.get_description(),
            self.get_log_file_name(),
            self.model.num_mutations(),
        )

    def _end_message(self):
        tested = self._test_list.get_progress()
        self.logger.info(
            '''
                         --------------------------------------------------
                         Finished fuzzing session
                         Target: %s

                         Tested %d mutation%s
                         Failure count: %d
                         --------------------------------------------------
            ''',
            self.target.get_description(),
            tested,
            's' if tested > 1 else '',
            self.session_info.failure_count
        )

    def _test_info(self):
        fuzz_node_info = self.model.get_test_info()
        self.logger.info('Current test: %s' % self.model.current_index())
        self.logger.debug('----------------------------------------------')
        keys = sorted(fuzz_node_info.keys())
        keys = [k for k in keys if k.startswith('node/field')]
        keys = [k for k in keys if not isinstance(fuzz_node_info[k], bool)]
        key_max_len = 0
        for key in keys:
            if len(key) > key_max_len:
                key_max_len = len(str(key))
        for k in keys:
            v = str(fuzz_node_info[k])
            k = str(k)
            pad = ' ' * (key_max_len - len(k) + 1)
            if len(v) > 70:
                v = v[:70] + '...'
            self.logger.debug('%s:%s%s' % (k, pad, v))
        self.logger.debug('----------------------------------------------')

    def _check_pause(self):
        if not self._continue_event.is_set():
            self.logger.info('fuzzer paused, waiting for resume command from user')
            self._continue_event.wait()
            self.logger.info('resume command received, continue running')

    def stop(self):
        '''
        stop the fuzzing session
        '''
        assert(self.model)
        assert(self.user_interface)
        assert(self.target)
        self.user_interface.stop()
        self.target.teardown()
        self.dataman.submit_task(None)
        self._un_set_signal_handler()

    def _store_report(self, report):
        self.logger.debug('<in>')
        report.add('test_number', self.model.current_index())
        report.add('fuzz_path', self.model.get_sequence_str())
        test_info = self.model.get_test_info()
        data_model_report = Report(name='Data Model')
        for k, v in test_info.items():
            new_entries = _flatten_dict_entry(k, v)
            for (k_, v_) in new_entries:
                data_model_report.add(k_, v_)
        report.add(data_model_report.get_name(), data_model_report)
        payload = self._last_payload
        if payload is not None:
            data_report = Report('payload')
            data_report.add('raw', payload)
            data_report.add('hex', hexlify(payload).decode())
            data_report.add('length', len(payload))
            report.add('payload', data_report)
        else:
            report.add('payload', None)

        self.dataman.store_report(report, self.model.current_index())
        self.dataman.get_report_by_id(self.model.current_index())

    def _store_session(self):
        self._set_session_info()

    def _get_session_info(self):
        info = self.dataman.get_session_info()
        return info

    def _get_test_info(self):
        info = self.dataman.get('test_info')
        return info

    def _set_session_info(self):
        self.dataman.set_session_info(self.session_info)
        self.dataman.set('fuzzer_name', self.get_name())
        self.dataman.set('session_file_name', self.config.session_file_name)

    def _load_session(self):
        if not self.config.session_file_name:
            self.config.session_file_name = ':memory:'
        self.dataman = DataManager(self.config.session_file_name)
        self.dataman.start()
        if self.model:
            self.handle_stage_changed(self.model)
        self.dataman.set('log_file_name', self.get_log_file_name())
        info = self._get_session_info()
        if info:
            self.logger.info('Loaded session from DB')
            self.session_info = info
            return True
        self.logger.info('No session loaded')
        self._set_session_info()
        return False

    def _exit_now(self, dummy1, dummy2):
        self.stop()
        sys.exit(0)

    def _keep_running(self):
        '''
        Should we still fuzz??
        '''
        if self.config.max_failures:
            if self.session_info.failure_count >= self.config.max_failures:
                return False
        return self._test_list.current() is not None

    def _set_signal_handler(self):
        '''
        Replace the signal handler with self._exit_now
        '''
        import signal
        signal.signal(signal.SIGINT, self._exit_now)

    @classmethod
    def _un_set_signal_handler(cls):
        '''
        Set the default signal handler
        '''
        import signal
        signal.signal(signal.SIGINT, signal.SIG_DFL)
Esempio n. 8
0
    def start(self):
        '''
        Start the fuzzing session

        If fuzzer already running, it will return immediatly
        '''
        if self._started:
            self.logger.warning('called while fuzzer is running. ignoring.')
            return
        self._started = True
        assert(self.model)
        assert(self.user_interface)
        assert(self.target)

        if self._load_session():
            self._check_session_validity()
            self._set_test_ranges(
                self.session_info.start_index,
                self.session_info.end_index,
                self.session_info.test_list_str
            )
        else:
            self.session_info.kitty_version = _get_current_version()
            # TODO: write hash for high level
            self.session_info.data_model_hash = self.model.hash()
        # if self.session_info.end_index is None:
        #     self.session_info.end_index = self.model.last_index()
        if self._test_list is None:
            self._test_list = StartEndList(0, self.model.num_mutations())
        else:
            self._test_list.set_last(self.model.last_index())

        list_count = self._test_list.get_count()
        self._test_list.skip(list_count - 1)
        self.session_info.end_index = self._test_list.current()
        self._test_list.reset()
        self._store_session()
        self._test_list.skip(self.session_info.current_index)
        self.session_info.test_list_str = self._test_list.as_test_list_str()

        self._set_signal_handler()
        self.user_interface.set_data_provider(self.dataman)
        self.user_interface.set_continue_event(self._continue_event)
        self.user_interface.start()

        self.session_info.start_time = time.time()
        try:
            self._start_message()
            self.target.setup()
            start_from = self.session_info.current_index
            if self._skip_env_test:
                self.logger.info('Skipping environment test')
            else:
                self.logger.info('Performing environment test')
                self._test_environment()
            self._in_environment_test = False
            self._test_list.reset()
            self._test_list.skip(start_from)
            self.session_info.current_index = start_from
            self.model.skip(self._test_list.current())
            self._start()
            return True
        except Exception as e:
            self.logger.error('Error occurred while fuzzing: %s', repr(e))
            self.logger.error(traceback.format_exc())
            return False