def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzz session. Args: target_path: Path to the target. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A FuzzResult object. """ config = launcher.AflConfig.from_target_path(target_path) config.additional_afl_arguments = options.arguments testcase_file_path = os.path.join(reproducers_dir, 'crash') runner = launcher.prepare_runner(target_path, config, testcase_file_path, options.corpus_dir, max_time, strategy_dict=options.strategies) fuzz_result = runner.fuzz() command = fuzz_result.command time_executed = fuzz_result.time_executed fuzzing_logs = fuzz_result.output + runner.fuzzer_stderr # Bail out if AFL returns a nonzero status code. if fuzz_result.return_code: target = engine_common.get_project_qualified_fuzzer_name( target_path) logs.log_error( f'afl: engine encountered an error (target={target})', engine_output=fuzz_result.output) return engine.FuzzResult(fuzzing_logs, command, [], {}, time_executed) stats_getter = stats.StatsGetter(runner.afl_output.stats_path, config.dict_path) new_units_generated, new_units_added, corpus_size = ( runner.libfuzzerize_corpus()) stats_getter.set_stats(fuzz_result.time_executed, new_units_generated, new_units_added, corpus_size, runner.strategies, runner.fuzzer_stderr, fuzz_result.output) crashes = [] if os.path.exists(testcase_file_path): crash = engine.Crash(testcase_file_path, runner.fuzzer_stderr, [], fuzz_result.time_executed) crashes.append(crash) return engine.FuzzResult(fuzzing_logs, command, crashes, stats_getter.stats, time_executed)
def engine_fuzz(engine_impl, target_name, sync_corpus_directory, testcase_directory): """Run engine fuzzer on untrusted worker.""" request = untrusted_runner_pb2.EngineFuzzRequest( engine=engine_impl.name, target_name=target_name, sync_corpus_directory=file_host.rebase_to_worker_root( sync_corpus_directory), testcase_directory=file_host.rebase_to_worker_root(testcase_directory)) response = host.stub().EngineFuzz(request) crashes = [ engine.Crash(input_path=file_host.rebase_to_host_root( crash.input_path), stacktrace=crash.stacktrace, reproduce_args=crash.reproduce_args, crash_time=crash.crash_time) for crash in response.crashes ] unpacked_stats = _unpack_values(response.stats) unpacked_strategies = _unpack_values(response.strategies) result = engine.FuzzResult(logs=response.logs, command=list(response.command), crashes=crashes, stats=unpacked_stats, time_executed=response.time_executed) file_host.pull_testcases_from_worker() return result, dict(response.fuzzer_metadata), unpacked_strategies
def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzzing session. Args: target_path: Path to the fuzzer script or binary. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A FuzzResult object. """ # For blackbox fuzzers, |target_path| supplies the path to the fuzzer script # rather than a target in the build archive. fuzzer_path = target_path os.chmod(fuzzer_path, 0o775) app_path = environment.get_value('APP_PATH') app_args = testcase_manager.get_command_line_for_application( get_arguments_only=True).strip() corpus_dir = options.corpus_dir command_line_args = _get_arguments(app_path, app_args) command_line_args.append(f'--input_dir={corpus_dir}') result = _run_with_interpreter_if_needed(fuzzer_path, command_line_args, max_time) crashes = [] for testcase_path in os.listdir(reproducers_dir): if not testcase_path.startswith(TESTCASE_PREFIX): continue output_path = OUTPUT_PREFIX + testcase_path[len(TESTCASE_PREFIX):] absolute_output_path = os.path.join(reproducers_dir, output_path) # If no output was written for a test case, skip it. if not os.path.exists(absolute_output_path): continue with open(absolute_output_path, 'r', errors='replace') as handle: output = handle.read() # Filter obviously non-crashing test cases. Crashes still follow the # normal flow in fuzz task to ensure that the state should not be ignored # for other reasons, but we don't want to log every test case for the # fuzzers that don't do their own crash processing. state = stack_analyzer.get_crash_data(output) if not state.crash_type: continue full_testcase_path = os.path.join(reproducers_dir, testcase_path) crash = engine.Crash(full_testcase_path, output, options.arguments, int(result.time_executed)) crashes.append(crash) # TODO(mbarbella): Support stats. stats = {} return engine.FuzzResult(result.output, result.command, crashes, stats, result.time_executed)
def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzz session. Args: target_path: Path to the target. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A FuzzResult object. """ runner = _get_runner() arguments = _DEFAULT_ARGUMENTS[:] arguments.extend(options.arguments) arguments.extend([ '--input', options.corpus_dir, '--workspace', reproducers_dir, '--run_time', str(max_time), '--', target_path, ]) fuzz_result = runner.run_and_wait( additional_args=arguments, timeout=max_time + _CLEAN_EXIT_SECS) log_lines = fuzz_result.output.splitlines() sanitizer_stacktrace = _find_sanitizer_stacktrace(reproducers_dir) crashes = [] stats = None for line in log_lines: reproducer_path = _get_reproducer_path(line) if reproducer_path: crashes.append( engine.Crash(reproducer_path, sanitizer_stacktrace or '', [], int(fuzz_result.time_executed))) continue stats = _get_stats(line) if stats is None: stats = {} return engine.FuzzResult(fuzz_result.output, fuzz_result.command, crashes, stats, fuzz_result.time_executed)
def minimize_corpus(self, target_path, arguments, input_dirs, output_dir, reproducers_dir, max_time): """Optional (but recommended): run corpus minimization. Args: target_path: Path to the target. arguments: Additional arguments needed for corpus minimization. input_dirs: Input corpora. output_dir: Output directory to place minimized corpus. reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the minimization. Returns: A Result object. Raises: TimeoutError: If the corpus minimization exceeds max_time. Error: If the merge failed in some other way. """ runner = libfuzzer.get_runner(target_path) libfuzzer.set_sanitizer_options(target_path) merge_tmp_dir = self._create_temp_corpus_dir('merge-workdir') result = runner.merge([output_dir] + input_dirs, merge_timeout=max_time, tmp_dir=merge_tmp_dir, additional_args=arguments, artifact_prefix=reproducers_dir, merge_control_file=getattr( self, '_merge_control_file', None)) if result.timed_out: raise TimeoutError('Merging new testcases timed out\n' + result.output) if result.return_code != 0: raise MergeError('Merging new testcases failed: ' + result.output) merge_output = result.output merge_stats = stats.parse_stats_from_merge_log( merge_output.splitlines()) # TODO(ochang): Get crashes found during merge. return engine.FuzzResult(merge_output, result.command, [], merge_stats, result.time_executed)
def fuzz(self, fuzz_timeout, additional_args, unused_additional_args=None, unused_extra_env=None): """This is where actual syzkaller fuzzing is done. Args: fuzz_timeout: The maximum time in seconds that fuzz job is allowed to run for. additional_args: A sequence of additional arguments to be passed to the executable. """ def _filter_log(content): """Filter unneeded content from log.""" result = '' strip_regex = re.compile(r'^c\d+\s+\d+\s') for line in content.splitlines(): result += strip_regex.sub('', line) + '\n' return result logs.log('Running Syzkaller.') additional_args = copy.copy(additional_args) fuzz_result = self.run_and_wait(additional_args, timeout=fuzz_timeout) logs.log('Syzkaller stopped, fuzzing timed out: {}'.format( fuzz_result.time_executed)) fuzz_logs = (fuzz_result.output or '') + '\n' crashes = [] parsed_stats = {} visited = set() for subdir, _, files in os.walk(get_work_dir()): for file in files: # Each crash typically have 2 files: reportN and logN. Similar crashes # are grouped together in subfolders. unique_crash puts together the # subfolder name and reportN. unique_crash = os.path.join(subdir, file) if fnmatch.fnmatch(file, 'report*') and unique_crash not in visited: visited.add(unique_crash) log_content = _filter_log( utils.read_data_from_file( os.path.join(subdir, file), eval_data=False).decode('utf-8')) fuzz_logs += log_content + '\n' # Since each crash (report file) has a corresponding log file # that contains the syscalls that caused the crash. This file is # located in the same subfolder and has the same number. # E.g. ./439c37d288d4f26a33a6c7e5c57a97791453a447/report15 and # ./439c37d288d4f26a33a6c7e5c57a97791453a447/log15. crash_testcase_file_path = os.path.join( subdir, 'log' + file[len('report'):]) # TODO(hzawawy): Parse stats information and add them to FuzzResult. if crash_testcase_file_path: reproduce_arguments = [unique_crash] actual_duration = int(fuzz_result.time_executed) # Write the new testcase. # Copy crash testcase contents into the main testcase path. crashes.append( engine.Crash(crash_testcase_file_path, log_content, reproduce_arguments, actual_duration)) return engine.FuzzResult(fuzz_logs, fuzz_result.command, crashes, parsed_stats, fuzz_result.time_executed)
def _minimize_corpus_two_step(self, target_path, arguments, existing_corpus_dirs, new_corpus_dir, output_corpus_dir, reproducers_dir, max_time): """Optional (but recommended): run corpus minimization. Args: target_path: Path to the target. arguments: Additional arguments needed for corpus minimization. existing_corpus_dirs: Input corpora that existed before the fuzzing run. new_corpus_dir: Input corpus that was generated during the fuzzing run. Must have at least one new file. output_corpus_dir: Output directory to place minimized corpus. reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the minimization. Returns: A Result object. """ if not _is_multistep_merge_supported(target_path): # Fallback to the old single step merge. It does not support incremental # stats and provides only `edge_coverage` and `feature_coverage` stats. logs.log('Old version of libFuzzer is used. Using single step merge.') return self.minimize_corpus(target_path, arguments, existing_corpus_dirs + [new_corpus_dir], output_corpus_dir, reproducers_dir, max_time) # The dir where merge control file is located must persist for both merge # steps. The second step re-uses the MCF produced during the first step. merge_control_file_dir = self._create_temp_corpus_dir('mcf_tmp_dir') self._merge_control_file = os.path.join(merge_control_file_dir, 'MCF') # Two step merge process to obtain accurate stats for the new corpus units. # See https://reviews.llvm.org/D66107 for a more detailed description. merge_stats = {} # Step 1. Use only existing corpus and collect "initial" stats. result_1 = self.minimize_corpus(target_path, arguments, existing_corpus_dirs, output_corpus_dir, reproducers_dir, max_time) merge_stats['initial_edge_coverage'] = result_1.stats['edge_coverage'] merge_stats['initial_feature_coverage'] = result_1.stats['feature_coverage'] # Clear the output dir as it does not have any new units at this point. engine_common.recreate_directory(output_corpus_dir) # Adjust the time limit for the time we spent on the first merge step. max_time -= result_1.time_executed if max_time <= 0: raise TimeoutError('Merging new testcases timed out\n' + result_1.logs) # Step 2. Process the new corpus units as well. result_2 = self.minimize_corpus( target_path, arguments, existing_corpus_dirs + [new_corpus_dir], output_corpus_dir, reproducers_dir, max_time) merge_stats['edge_coverage'] = result_2.stats['edge_coverage'] merge_stats['feature_coverage'] = result_2.stats['feature_coverage'] # Diff the stats to obtain accurate values for the new corpus units. merge_stats['new_edges'] = ( merge_stats['edge_coverage'] - merge_stats['initial_edge_coverage']) merge_stats['new_features'] = ( merge_stats['feature_coverage'] - merge_stats['initial_feature_coverage']) output = result_1.logs + '\n\n' + result_2.logs if (merge_stats['new_edges'] < 0 or merge_stats['new_features'] < 0): logs.log_error( 'Two step merge failed.', merge_stats=merge_stats, output=output) merge_stats['new_edges'] = 0 merge_stats['new_features'] = 0 self._merge_control_file = None # TODO(ochang): Get crashes found during merge. return engine.FuzzResult(output, result_2.command, [], merge_stats, result_1.time_executed + result_2.time_executed)
def fuzz(self, target_path, options, reproducers_dir, max_time): """Run a fuzz session. Args: target_path: Path to the target. options: The FuzzOptions object returned by prepare(). reproducers_dir: The directory to put reproducers in when crashes are found. max_time: Maximum allowed time for the fuzzing to run. Returns: A FuzzResult object. """ profiler.start_if_needed('libfuzzer_fuzz') runner = libfuzzer.get_runner(target_path) libfuzzer.set_sanitizer_options(target_path, fuzz_options=options) # Directory to place new units. new_corpus_dir = self._create_temp_corpus_dir('new') corpus_directories = [new_corpus_dir] + options.fuzz_corpus_dirs fuzz_result = runner.fuzz( corpus_directories, fuzz_timeout=max_time, additional_args=options.arguments, artifact_prefix=reproducers_dir, extra_env=options.extra_env) project_qualified_fuzzer_name = ( engine_common.get_project_qualified_fuzzer_name(target_path)) dict_error_match = DICT_PARSING_FAILED_REGEX.search(fuzz_result.output) if dict_error_match: logs.log_error( 'Dictionary parsing failed (target={target}, line={line}).'.format( target=project_qualified_fuzzer_name, line=dict_error_match.group(1)), engine_output=fuzz_result.output) elif (not environment.get_value('USE_MINIJAIL') and fuzz_result.return_code == constants.LIBFUZZER_ERROR_EXITCODE): # Minijail returns 1 if the exit code is nonzero. # Otherwise: we can assume that a return code of 1 means that libFuzzer # itself ran into an error. logs.log_error( ENGINE_ERROR_MESSAGE + ' (target={target}).'.format(target=project_qualified_fuzzer_name), engine_output=fuzz_result.output) log_lines = fuzz_result.output.splitlines() # Output can be large, so save some memory by removing reference to the # original output which is no longer needed. fuzz_result.output = None # Check if we crashed, and get the crash testcase path. crash_testcase_file_path = runner.get_testcase_path(log_lines) # If we exited with a non-zero return code with no crash file in output from # libFuzzer, this is most likely a startup crash. Use an empty testcase to # to store it as a crash. if not crash_testcase_file_path and fuzz_result.return_code: crash_testcase_file_path = self._create_empty_testcase_file( reproducers_dir) # Parse stats information based on libFuzzer output. parsed_stats = libfuzzer.parse_log_stats(log_lines) # Extend parsed stats by additional performance features. parsed_stats.update( stats.parse_performance_features(log_lines, options.strategies, options.arguments)) # Set some initial stat overrides. timeout_limit = fuzzer_utils.extract_argument( options.arguments, constants.TIMEOUT_FLAG, remove=False) expected_duration = runner.get_max_total_time(max_time) actual_duration = int(fuzz_result.time_executed) fuzzing_time_percent = 100 * actual_duration / float(expected_duration) parsed_stats.update({ 'timeout_limit': int(timeout_limit), 'expected_duration': expected_duration, 'actual_duration': actual_duration, 'fuzzing_time_percent': fuzzing_time_percent, }) # Remove fuzzing arguments before merge and dictionary analysis step. merge_arguments = options.arguments[:] libfuzzer.remove_fuzzing_arguments(merge_arguments, is_merge=True) self._merge_new_units(target_path, options.corpus_dir, new_corpus_dir, options.fuzz_corpus_dirs, merge_arguments, parsed_stats) fuzz_logs = '\n'.join(log_lines) crashes = [] if crash_testcase_file_path: reproduce_arguments = options.arguments[:] libfuzzer.remove_fuzzing_arguments(reproduce_arguments) # Use higher timeout for reproduction. libfuzzer.fix_timeout_argument_for_reproduction(reproduce_arguments) # Write the new testcase. # Copy crash testcase contents into the main testcase path. crashes.append( engine.Crash(crash_testcase_file_path, fuzz_logs, reproduce_arguments, actual_duration)) libfuzzer.analyze_and_update_recommended_dictionary( runner, project_qualified_fuzzer_name, log_lines, options.corpus_dir, merge_arguments) return engine.FuzzResult(fuzz_logs, fuzz_result.command, crashes, parsed_stats, fuzz_result.time_executed)