-
Notifications
You must be signed in to change notification settings - Fork 546
/
launcher.py
executable file
·1734 lines (1367 loc) · 62 KB
/
launcher.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Launcher script for afl++ based fuzzers."""
# pylint: disable=g-statement-before-imports
try:
# ClusterFuzz dependencies.
from clusterfuzz._internal.base import modules
modules.fix_module_search_paths()
except ImportError:
pass
import atexit
import collections
import enum
import os
import re
import shutil
import signal
import stat
import subprocess
import sys
from clusterfuzz._internal.base import utils
from clusterfuzz._internal.bot.fuzzers import dictionary_manager
from clusterfuzz._internal.bot.fuzzers import engine_common
from clusterfuzz._internal.bot.fuzzers import options
from clusterfuzz._internal.bot.fuzzers import strategy_selection
from clusterfuzz._internal.bot.fuzzers import utils as fuzzer_utils
from clusterfuzz._internal.bot.fuzzers.afl import constants
from clusterfuzz._internal.bot.fuzzers.afl import stats
from clusterfuzz._internal.bot.fuzzers.afl.fuzzer import write_dummy_file
from clusterfuzz._internal.fuzzing import strategy
from clusterfuzz._internal.metrics import logs
from clusterfuzz._internal.metrics import profiler
from clusterfuzz._internal.platforms import android
from clusterfuzz._internal.system import environment
from clusterfuzz._internal.system import new_process
from clusterfuzz._internal.system import shell
# Allow 30 minutes to merge the testcases back into the corpus. This matches
# libFuzzer's merge timeout.
DEFAULT_MERGE_TIMEOUT = 30 * 60
BOT_NAME = environment.get_value('BOT_NAME', '')
STDERR_FILENAME = 'stderr.out'
MAX_OUTPUT_LEN = 1 * 1024 * 1024 # 1 MB
# .options file option for the number of persistent executions.
PERSISTENT_EXECUTIONS_OPTION = 'n'
# Grace period for the launcher to complete any processing before it's killed.
# This will no longer be needed when we migrate to the engine interface.
POSTPROCESSING_TIMEOUT = 30
class AflOptionType(enum.Enum):
ARG = 0
ENV_VAR = 1
# Afl options have names and can either be commandline arguments or environment
# variables.
AflOption = collections.namedtuple('AflOption', ['name', 'type'])
class AflConfig:
"""Helper class that determines the arguments that should be passed to
afl-fuzz, environment variables that should be set before running afl-fuzz,
and the number of persistent executions that should be passed to the target
Determines these mainly by parsing the .options file for the target."""
# Mapping of libfuzzer option names to AflOption objects.
LIBFUZZER_TO_AFL_OPTIONS = {
'dict':
AflOption(constants.DICT_FLAG, AflOptionType.ARG),
'close_fd_mask':
AflOption(constants.CLOSE_FD_MASK_ENV_VAR, AflOptionType.ENV_VAR),
}
def __init__(self):
"""Sets the configs to sane defaults. Use from_target_path if you want to
use the .options file and possibly .dict to set the configs."""
self.additional_afl_arguments = []
self.additional_env_vars = {}
self.num_persistent_executions = constants.MAX_PERSISTENT_EXECUTIONS
@classmethod
def from_target_path(cls, target_path):
"""Instantiates and returns an AFLConfig object. The object is configured
based on |target_path|."""
config = cls()
config.parse_options(target_path)
config.dict_path = fuzzer_utils.extract_argument(
config.additional_afl_arguments, constants.DICT_FLAG, remove=False)
config.use_default_dict(target_path)
dictionary_manager.correct_if_needed(config.dict_path)
return config
def parse_options(self, target_path):
"""Parses a target's .options file (determined using |target_path|) if it
exists and sets configs based on it."""
fuzzer_options = options.get_fuzz_target_options(target_path)
if not fuzzer_options:
return
self.additional_env_vars = fuzzer_options.get_env()
# Try to convert libFuzzer arguments to AFL arguments or env vars.
libfuzzer_options = fuzzer_options.get_engine_arguments('libfuzzer')
for libfuzzer_name, value in libfuzzer_options.dict().items():
if libfuzzer_name not in self.LIBFUZZER_TO_AFL_OPTIONS:
continue
afl_option = self.LIBFUZZER_TO_AFL_OPTIONS[libfuzzer_name]
if afl_option.type == AflOptionType.ARG:
self.additional_afl_arguments.append(f'{afl_option.name}{value}')
else:
assert afl_option.type == AflOptionType.ENV_VAR
self.additional_env_vars[afl_option.name] = value
# Get configs set specifically for AFL.
afl_options = fuzzer_options.get_engine_arguments('AFL')
self.num_persistent_executions = afl_options.get(
PERSISTENT_EXECUTIONS_OPTION, constants.MAX_PERSISTENT_EXECUTIONS)
def use_default_dict(self, target_path):
"""Set the dictionary argument in |self.additional_afl_arguments| to
%target_binary_name%.dict if no dictionary argument is already specified.
Also update |self.dict_path|."""
if self.dict_path:
return
default_dict_path = dictionary_manager.get_default_dictionary_path(
target_path)
if not os.path.exists(default_dict_path):
return
if not environment.is_android():
self.dict_path = default_dict_path
else:
self.dict_path = android.util.get_device_path(default_dict_path)
self.additional_afl_arguments.append(constants.DICT_FLAG + self.dict_path)
class AflFuzzOutputDirectory:
"""Helper class used by AflRunner to deal with AFL's output directory and its
contents (ie: the -o argument to afl-fuzz)."""
# AFL usually copies over old units from the corpus to the queue and adds the
# string 'orig' to the new filename. Therefore we know that testcases
# containing 'orig' are copied.
COPIED_FILE_STRING = 'orig'
TESTCASE_REGEX = re.compile(r'id:\d{6},.+')
def __init__(self):
self.output_directory = os.path.join(fuzzer_utils.get_temp_dir(),
'afl_output_dir')
engine_common.recreate_directory(self.output_directory)
@classmethod
def is_testcase(cls, path):
"""Is the path an AFL testcase file or something else."""
return (os.path.isfile(path) and
bool(re.match(cls.TESTCASE_REGEX, os.path.basename(path))))
@property
def instance_directory(self):
"""Returns afl-fuzz's instance directory."""
return os.path.join(self.output_directory, constants.DEFAULT_INSTANCE_ID)
@property
def queue(self):
"""Returns afl-fuzz's queue directory."""
return os.path.join(self.instance_directory, 'queue')
def is_new_testcase(self, path):
"""Determine if |path| is a new unit."""
# Clearly non-testcases can't be new testcases.
return (self.is_testcase(path) and
self.COPIED_FILE_STRING not in os.path.basename(path))
def count_new_units(self, corpus_path):
"""Count the number of new units (testcases) in |corpus_path|."""
corpus_files = os.listdir(corpus_path)
num_new_units = 0
for testcase in corpus_files:
if self.is_new_testcase(os.path.join(corpus_path, testcase)):
num_new_units += 1
return num_new_units
def copy_crash_if_needed(self, testcase_path):
"""Copy the first crash found by AFL to |testcase_path| (the input file
created by run.py).
"""
crash_paths = list_full_file_paths(
os.path.join(self.instance_directory, 'crashes'))
for crash_path in crash_paths:
# AFL puts a README.txt file in the crashes directory. Just ignore it.
if self.is_testcase(crash_path):
shutil.copyfile(crash_path, testcase_path)
break
def remove_hang_in_queue(self, hang_filename):
"""Removes the hanging testcase from queue."""
# AFL copies all inputs to the queue and renames them in the format
# "id:NUMBER,orig:$hang_filename". So remove that file from the queue.
# TODO(metzman): What about for the copied inputs without 'orig' in their
# name that we have been seeing? Does this work anyway because AFL uses the
# the full name of the file in the queue?
queue_paths = list_full_file_paths(self.queue)
hang_queue_path = [
path for path in queue_paths if path.endswith(hang_filename)
][0]
remove_path(hang_queue_path)
@property
def stats_path(self):
"""Returns the path of AFL's stats file: "fuzzer_stats"."""
return os.path.join(self.instance_directory, 'fuzzer_stats')
class AflAndroidFuzzOutputDirectory(AflFuzzOutputDirectory):
"""Helper class used by AflAndroidRunner to deal with AFL's output directory
and help copy contents from device to local.
"""
def remove_hang_in_queue(self, hang_filename):
"""Removes the hanging testcase from queue."""
queue_paths = list_full_file_paths_device(self.queue)
hang_queue_path = [
path for path in queue_paths if path.endswith(hang_filename)
][0]
hang_queue_path_device = android.util.get_device_path(hang_queue_path)
android.adb.remove_file(hang_queue_path_device)
def copy_crash_if_needed(self, testcase_path):
"""Copy the first crash found by AFL. Before calling super method
copy the crashes directory from device to local.
"""
logs.log('Copying crash directory from device to local.')
# Copy the crashes dir from device to local.
local_directory = os.path.join(self.instance_directory, 'crashes')
device_directory = android.util.get_device_path(local_directory)
shell.remove_directory(local_directory, recreate=True)
android.adb.copy_remote_directory_to_local(device_directory,
local_directory)
super().copy_crash_if_needed(testcase_path)
class FuzzingStrategies:
"""Helper class used by AflRunner classes to decide what strategy to use
and to record the decision for StatsGetter to use later."""
# Probability for the level of CMPLOG to set. (-l X)
CMPLOG_LEVEL_PROBS = [
('1', 0.7), # Level 1
('2', 0.25), # Level 2
('3', 0.05), # Level 3
]
# Probabilty for arithmetic CMPLOG calculations.
CMPLOG_ARITH_PROB = 0.4
# Probability for transforming CMPLOG solving.
CMPLOG_TRANS_PROB = 0.1
# Probability for extreme CMPLOG solving.
CMPLOG_XTREME_PROB = 0.05
# Probability for randomized coloring.
CMPLOG_RAND_PROB = 0.1
# Probability to disable trimming. (AFL_DISABLE_TRIM=1)
DISABLE_TRIM_PROB = 0.70
# Probability to keep long running finds. (AFL_KEEP_TIMEOUTS=1)
KEEP_TIMEOUTS_PROB = 0.7
# Probability for increased havoc intensity. (AFL_EXPAND_HAVOC_NOW=1)
EXPAND_HAVOC_PROB = 0.5
# Probability for a fixed mutation type. (-P)
MUTATION_PROB = 0.5
MUTATION_EXPLORE_PROB = 0.75
# PROBABILITY for input type. (-a)
INPUT_PROB = 0.4
INPUT_ASCII_PROB = 0.3
# Probability to use the MOpt mutator. (-L0)
MOPT_PROB = 0.4
# Probability to use the original afl queue walking mechanism. (-Z)
QUEUE_OLD_STRATEGY_PROB = 0.2
# Probability to ignore long running inputs. (AFL_IGNORE_TIMEOUTS=1)
IGNORE_TIMEOUTS_PROB = 0.7
# Probability to enable the schedule cycler. (AFL_CYCLE_SCHEDULES=1)
SCHEDULER_CYCLE_PROB = 0.1
# Propability which scheduler to select. (-p SCHEDULER)
SCHEDULER_PROBS = [
('fast', .3),
('explore', .3),
('exploit', .2),
('coe', .1),
('rare', .1),
]
# TODO(mbarbella): The codepath involving |strategy_dict| and the
# to_strategy_dict function should be removed when everything is fully
# converted to the engine pipeline. For now this allows the code to be shared
# between both cases while still adhering to the new pipeline's API.
def __init__(self, target_path, strategy_dict=None):
self.corpus_subset_size = None
self.candidate_generator = engine_common.Generator.NONE
# If we have already generated a strategy dict, use that in favor of
# creating a new pool and picking randomly.
if strategy_dict is not None:
self.use_corpus_subset = 'corpus_subset' in strategy_dict
if self.use_corpus_subset:
self.corpus_subset_size = strategy_dict['corpus_subset']
if strategy_dict.get(strategy.CORPUS_MUTATION_RADAMSA_STRATEGY.name) == 1:
self.candidate_generator = engine_common.Generator.RADAMSA
else:
strategy_pool = strategy_selection.generate_weighted_strategy_pool(
strategy_list=strategy.AFL_STRATEGY_LIST,
use_generator=True,
engine_name='afl')
# Select a generator to attempt to use for existing testcase mutations.
self.candidate_generator = engine_common.select_generator(
strategy_pool, target_path)
self.use_corpus_subset = strategy_pool.do_strategy(
strategy.CORPUS_SUBSET_STRATEGY)
if self.use_corpus_subset:
self.corpus_subset_size = engine_common.random_choice(
engine_common.CORPUS_SUBSET_NUM_TESTCASES)
self.is_mutations_run = (
self.candidate_generator != engine_common.Generator.NONE)
# Generator that is actually used. Initialize to none, change if new
# testcase mutations are properly generated by the candidate generator.
self.generator_strategy = engine_common.Generator.NONE
def to_strategy_dict(self):
"""Convert to a strategy dict in the format used by the engine pipeline."""
# The decision on whether or not fast cal should be used is made during
# fuzzing. This function is expected to be called whe preparing for fuzzing.
strategies_dict = {}
if self.generator_strategy == engine_common.Generator.RADAMSA:
strategies_dict[strategy.CORPUS_MUTATION_RADAMSA_STRATEGY.name] = 1
if self.use_corpus_subset:
strategies_dict['corpus_subset'] = self.corpus_subset_size
return strategies_dict
class AflFuzzInputDirectory:
"""Helper class used by AflRunner to deal with the input directory passed to
afl-fuzz as the -i argument.
"""
# If the number of input files is less than this, don't bother skipping
# deterministic steps since it won't take long.
MIN_INPUTS_FOR_SKIP = 10
MAX_COPIED_CORPUS_SIZE = 2**30 # 1 GB
def __init__(self, input_directory, target_path, fuzzing_strategies):
"""Inits AflFuzzInputDirectory.
Args:
input_directory: Directory passed to afl-fuzz containing corpus.
target_path: Path to the fuzzer executable. Used to find seed corpus.
fuzzing_strategies: fuzzing strategies to use.
"""
self.input_directory = input_directory
self.strategies = fuzzing_strategies
# We only need to use this when a temporary input directory is made.
# (ie: when there is an oversized testcase in the input).
self.original_input_directory = None
engine_common.unpack_seed_corpus_if_needed(
target_path, self.input_directory, max_bytes=constants.MAX_FILE_BYTES)
# Ensure there is a usable testcase in the input directory. This is needed
# because locally (and possibly for new fuzzers on CF) the dummy file is not
# always in the input directory, which prevents AFL from running.
if not list_full_file_paths(self.input_directory):
write_dummy_file(self.input_directory)
def restore_if_needed(self):
"""Restore the original input directory if self.original_input_directory is
set. Used to by merge() to get rid of the temporary input directory if it
exists and merge new units into the original input directory.
"""
if self.original_input_directory is None:
return
# Remove the current input directory as it was only temporary.
remove_path(self.input_directory)
self.input_directory = self.original_input_directory
self.original_input_directory = None
# pylint: disable=no-member
class AflRunnerCommon:
"""Afl runner common routines."""
# Window of time for afl to exit gracefully before we kill it.
AFL_CLEAN_EXIT_TIME = 10.0
# Time to wait for SIGTERM handler.
SIGTERM_WAIT_TIME = 10.0
# Maximum number of times we will retry fuzzing after fixing an issue.
MAX_FUZZ_RETRIES = 40
# Maximum number of times we will retry fuzzing with a strict autocalibrated
# timeout. After this number of fuzzing retries, if we see a hang we will set
# the timeout to to '-t' + str(self.MANUAL_TIMEOUT_MILLISECONDS) + '+' to tell
# AFL to skip testcases that take longer.
MAX_FUZZ_RETRIES_WITH_STRICT_TIMEOUT = 20
# The number of times we will retry fuzzing with the deferred fork server
# after the first testcase hangs. Afterwards we will use
# AFL_DRIVER_DONT_DEFER, since this is a common symptom of fuzzers that
# cant use the deferred forkserver.
MAX_FIRST_HANGS_WITH_DEFERRED_FORKSERVER = 5
# The timeout we will use if autocalibrating results in too many hangs. This
# is the maximum autocalibrated timeout afl-fuzz can set.
MANUAL_TIMEOUT_MILLISECONDS = 5000
# Regexes used to determine which file caused AFL to quit.
CRASH_REGEX = re.compile(
r'Test case \'id\:\d+,orig:(?P<orig_testcase_filename>.*)\' results in a'
' crash')
HANG_REGEX = re.compile(
r'Test case \'(?P<testcase_filename>.*)\' results in a (hang|timeout)')
CPU_BIND_ERROR_REGEX = re.compile('PROGRAM ABORT :.*No more free CPU cores')
# Log messages we format and log as error when afl-fuzz stops running.
CRASH_LOG_MESSAGE = 'Testcase {0} in corpus causes a crash'
HANG_LOG_MESSAGE = 'Testcase {0} in corpus causes a hang, retrying without it'
SHOWMAP_FILENAME = 'afl_showmap_output'
SHOWMAP_REGEX = re.compile(br'(?P<guard>\d{6}):(?P<hit_count>\d+)\n')
def __init__(self,
target_path,
config,
testcase_file_path,
input_directory,
timeout=None,
afl_tools_path=None,
strategy_dict=None):
"""Inits the AflRunner.
Args:
target_path: Path to the fuzz target.
config: AflConfig object.
testcase_file_path: File to write crashes to.
input_directory: Corpus directory passed to afl-fuzz.
afl_tools_path: Path that is used to locate afl-* tools.
"""
self.target_path = target_path
self.config = config
self.testcase_file_path = testcase_file_path
self._input_directory = input_directory
self.timeout = timeout
if afl_tools_path is None:
afl_tools_path = os.path.dirname(target_path)
# Set paths to afl tools.
self.afl_fuzz_path = os.path.join(afl_tools_path, 'afl-fuzz')
self.afl_showmap_path = os.path.join(afl_tools_path, 'afl-showmap')
self._afl_input = None
self._afl_output = None
self.strategies = FuzzingStrategies(
target_path, strategy_dict=strategy_dict)
# Set this to None so we can tell if it has never been set or if it's just
# empty.
self._fuzzer_stderr = None
self.initial_max_total_time = 0
for env_var, value in config.additional_env_vars.items():
environment.set_value(env_var, value)
self.showmap_output_path = os.path.join(fuzzer_utils.get_temp_dir(),
self.SHOWMAP_FILENAME)
self.merge_timeout = engine_common.get_merge_timeout(DEFAULT_MERGE_TIMEOUT)
self.showmap_no_output_logged = False
self._fuzz_args = []
@property
def stderr_file_path(self):
"""Returns the file for afl to output stack traces."""
return os.path.join(fuzzer_utils.get_temp_dir(), STDERR_FILENAME)
@property
def fuzzer_stderr(self):
"""Returns the stderr of the fuzzer. Reads it first if it wasn't already
read. Because ClusterFuzz terminates this process after seeing a stacktrace
printed, make sure that printing this property is the last code a program
expects to execute.
"""
if self._fuzzer_stderr is not None:
return self._fuzzer_stderr
try:
with open(self.stderr_file_path, 'rb') as file_handle:
stderr_data = utils.decode_to_unicode(
utils.read_from_handle_truncated(file_handle, MAX_OUTPUT_LEN))
self._fuzzer_stderr = get_first_stacktrace(stderr_data)
except OSError:
self._fuzzer_stderr = ''
return self._fuzzer_stderr
def set_environment_variables(self):
"""Sets environment variables needed by afl."""
# Tell afl_driver to duplicate stderr to STDERR_FILENAME.
# Environment variable names and values that must be set before running afl.
environment.set_value(constants.FORKSRV_INIT_TMOUT_ENV_VAR,
constants.FORKSERVER_TIMEOUT)
environment.set_value(constants.FAST_CAL_ENV_VAR, 1)
environment.set_value(constants.IGNORE_UNKNOWN_ENVS_ENV_VAR, 1)
environment.set_value(constants.SKIP_CRASHES_ENV_VAR, 1)
environment.set_value(constants.BENCH_UNTIL_CRASH_ENV_VAR, 1)
environment.set_value(constants.SKIP_CPUFREQ_ENV_VAR, 1)
environment.set_value(constants.IGNORE_SEED_PROBLEMS, 1)
stderr_file_path = self.stderr_file_path
if environment.is_android():
stderr_file_path = android.util.get_device_path(self.stderr_file_path)
environment.set_value(constants.STDERR_FILENAME_ENV_VAR, stderr_file_path)
def get_afl_environment_variables(self):
afl_environment_vars = []
for env_var in os.environ:
if env_var and env_var.startswith('AFL_'):
afl_environment_vars.append(env_var + '=' +
str(environment.get_value(env_var)))
return afl_environment_vars
def check_return_code(self, result, additional_return_codes=None):
expected_return_codes = [0, 1, -6]
if additional_return_codes:
expected_return_codes += additional_return_codes
if result.return_code not in expected_return_codes:
logs.log_error(
f'AFL target exited with abnormal exit code: {result.return_code}.',
output=result.output)
def run_single_testcase(self, testcase_path):
"""Runs a single testcase.
Args:
testcase_path: Path to testcase to be run.
Returns:
A new_process.ProcessResult.
"""
self._executable_path = self.target_path
assert not testcase_path.isdigit(), ('We don\'t want to specify number of'
' executions by accident.')
self.afl_setup()
result = self.run_and_wait(additional_args=[testcase_path])
print('Running command:', engine_common.get_command_quoted(result.command))
self.check_return_code(result)
return result
def afl_setup(self):
"""Make sure we can run afl. Delete any files that afl_driver needs to
create and set any environmnet variables it needs.
"""
self.set_environment_variables()
remove_path(self.stderr_file_path)
@staticmethod
def set_resume(afl_args):
"""Changes afl_args so afl-fuzz will resume fuzzing rather than restarting.
"""
return AflRunner.set_input_arg(afl_args, constants.RESUME_INPUT)
@staticmethod
def get_arg_index(afl_args, flag):
for idx, arg in enumerate(afl_args):
if arg.startswith(flag):
return idx
return -1
@classmethod
def set_arg(cls, afl_args, flag, value):
"""Sets the afl |flag| to |value| in |afl_args|. If |flag| is already
in |afl_args|, then the old value is replaced by |value|, otherwise |flag|
and |value| are added.
"""
idx = cls.get_arg_index(afl_args, flag)
if value:
new_arg = flag + str(value)
else:
new_arg = flag
# Arg is not already in afl_args, add it.
if idx == -1:
afl_args.insert(0, new_arg)
else:
afl_args[idx] = new_arg
return afl_args
@classmethod
def remove_arg(cls, afl_args, flag):
idx = cls.get_arg_index(afl_args, flag)
if idx == -1:
return
del afl_args[idx]
@classmethod
def set_input_arg(cls, afl_args, new_input_value):
"""Changes the input argument (-i) in |afl_args| to |new_input_value|."""
return cls.set_arg(afl_args, constants.INPUT_FLAG, new_input_value)
@classmethod
def set_timeout_arg(cls, afl_args, timeout_value, skip_hangs=False):
timeout_value = str(int(timeout_value))
if skip_hangs:
timeout_value += '+'
cls.set_arg(afl_args, constants.TIMEOUT_FLAG, timeout_value)
return afl_args
def do_offline_mutations(self):
"""Mutate the corpus offline using Radamsa."""
if not self.strategies.is_mutations_run:
return
# Generate new testcase mutations according to candidate generator. If
# testcase mutations are properly generated, set generator strategy
# accordingly.
generator_used = engine_common.generate_new_testcase_mutations(
self.afl_input.input_directory, self.afl_input.input_directory,
self.strategies.candidate_generator)
if generator_used:
self.strategies.generator_strategy = self.strategies.candidate_generator
# Delete large testcases created by generators.
for input_path in shell.get_files_list(self.afl_input.input_directory):
if os.path.getsize(input_path) >= constants.MAX_FILE_BYTES:
remove_path(input_path)
def generate_afl_args(self,
afl_input=None,
afl_output=None,
target_path=None,
additional_args=None,
mem_limit=constants.MAX_MEMORY_LIMIT):
"""Generate arguments to pass to Process.run_and_wait.
Args:
afl_input: Initial corpus directory passed as -i parameter to the afl
tool. Defaults to self.afl_input.input_directory.
afl_output: Output directory where afl stores corpus and stats, passed as
-o parameter to the afl tool. Defaults to
self.afl_output.output_directory.
target_path: Path target binary. Defaults to self.target_path.
additional_args: Additional AFL arguments
mem_limit: Virtual memory limit afl enforces on target binary, passed as
-m parameter to the afl tool. Defaults to constants.MAX_MEMORY_LIMIT.
Returns:
A list built from the function's arguments that can be passed as the
additional_args argument to Process.run_and_wait.
"""
if afl_input is None:
afl_input = self.afl_input.input_directory
if afl_output is None:
afl_output = self.afl_output.output_directory
if target_path is None:
target_path = self.target_path
afl_args = [
constants.INSTANCE_ID_FLAG + constants.DEFAULT_INSTANCE_ID,
constants.INPUT_FLAG + afl_input, constants.OUTPUT_FLAG + afl_output,
constants.MEMORY_LIMIT_FLAG + str(mem_limit)
]
if additional_args is not None:
afl_args += additional_args
afl_args.extend(self.config.additional_afl_arguments)
afl_args.extend([target_path, str(self.config.num_persistent_executions)])
return afl_args
def should_try_fuzzing(self, max_total_time, num_retries):
"""Returns True if we should try fuzzing, based on the number of times we've
already tried, |num_retries|, and the amount of time we have left
(calculated using |max_total_time|).
"""
if max_total_time <= 0:
logs.log_error('Tried fuzzing for {} seconds. Not retrying'.format(
self.initial_max_total_time))
return False
if num_retries > self.MAX_FUZZ_RETRIES:
logs.log_error(
'Tried to retry fuzzing {} times. Fuzzer is likely broken'.format(
num_retries))
return False
return True
def run_afl_fuzz(self, fuzz_args):
"""Run afl-fuzz and if there is an input that causes afl-fuzz to hang
or if it can't bind to a cpu, try fixing the issue and running afl-fuzz
again. If there is a crash in the starting corpus then report it.
Args:
fuzz_args: The arguments passed to afl-fuzz. List may be modified if
afl-fuzz runs into an error.
Returns:
A new_process.ProcessResult.
"""
# Define here to capture in closures.
max_total_time = self.initial_max_total_time
fuzz_result = None
def get_time_spent_fuzzing():
"""Gets the amount of time spent running afl-fuzz so far."""
return self.initial_max_total_time - max_total_time
def check_error_and_log(error_regex, log_message_format):
"""See if error_regex can match in fuzz_result.output. If it can, then it
uses the match to format and print log_message and return the match.
Otherwise returns None.
"""
matches = re.search(error_regex, fuzz_result.output)
if matches:
erroring_filename = matches.groups()[0]
message_format = (
'Seconds spent fuzzing: {seconds}, ' + log_message_format)
logs.log(
message_format.format(
erroring_filename, seconds=get_time_spent_fuzzing()))
return erroring_filename
return None # else
num_first_testcase_hangs = 0
num_retries = 0
while self.should_try_fuzzing(max_total_time, num_retries):
# Increment this now so that we can just "continue" without incrementing.
num_retries += 1
self.afl_setup()
# If the target was compiled for CMPLOG we need to set this.
build_directory = environment.get_value('BUILD_DIR')
cmplog_build_file = os.path.join(build_directory, 'afl_cmplog.txt')
if os.path.exists(cmplog_build_file):
self.set_arg(fuzz_args, constants.CMPLOG_FLAG, self.target_path)
# If a compile time dictionary was created - load it.
aflpp_dict_file = os.path.join(build_directory, 'afl++.dict')
if os.path.exists(aflpp_dict_file):
self.set_arg(fuzz_args, constants.DICT_FLAG, aflpp_dict_file)
# In the following section we randomly select different strategies.
# Randomly select a scheduler.
self.set_arg(fuzz_args, constants.SCHEDULER_FLAG,
rand_schedule(self.strategies.SCHEDULER_PROBS))
# Randomly set trimming vs no trimming.
if engine_common.decide_with_probability(
self.strategies.DISABLE_TRIM_PROB):
environment.set_value(constants.DISABLE_TRIM_ENV_VAR, 1)
# Randomly keep longer running testcases with new coverage.
if engine_common.decide_with_probability(
self.strategies.KEEP_TIMEOUTS_PROB):
environment.set_value(constants.KEEP_TIMEOUTS_ENV_VAR, 1)
# Randomly enable expanded havoc mutation.
if engine_common.decide_with_probability(
self.strategies.EXPAND_HAVOC_PROB):
environment.set_value(constants.EXPAND_HAVOC_NOW_VAR, 1)
# Always CMPLOG only new finds.
environment.set_value(constants.CMPLOG_ONLY_NEW_ENV_VAR, 1)
# Projects should rather be set up they work effectivly with afl++
# though for the time being lets allow bad setups to continue working:
environment.set_value(constants.IGNORE_PROBLEMS_ENV_VAR, 1)
# Randomly set to ignore long running inputs.
if engine_common.decide_with_probability(
self.strategies.IGNORE_TIMEOUTS_PROB):
environment.set_value(constants.IGNORE_TIMEOUTS_ENV_VAR, 1)
# Randomly set new vs. old queue selection mechanism.
if engine_common.decide_with_probability(
self.strategies.QUEUE_OLD_STRATEGY_PROB):
self.set_arg(fuzz_args, constants.QUEUE_OLD_STRATEGY_FLAG, None)
# Randomly select the MOpt mutator.
if engine_common.decide_with_probability(self.strategies.MOPT_PROB):
self.set_arg(fuzz_args, constants.MOPT_FLAG, '0')
# Select the CMPLOG level (even if no cmplog is used, it does not hurt).
self.set_arg(fuzz_args, constants.CMPLOG_LEVEL_FLAG,
rand_cmplog_level(self.strategies))
if engine_common.decide_with_probability(self.strategies.MUTATION_PROB):
if engine_common.decide_with_probability(
self.strategies.MUTATION_EXPLORE_PROB):
self.set_arg(fuzz_args, constants.MUTATION_STATE_FLAG,
constants.MUTATION_EXPLORE)
else:
self.set_arg(fuzz_args, constants.MUTATION_STATE_FLAG,
constants.MUTATION_EXPLOIT)
if engine_common.decide_with_probability(self.strategies.INPUT_PROB):
if engine_common.decide_with_probability(
self.strategies.INPUT_ASCII_PROB):
self.set_arg(fuzz_args, constants.INPUT_TYPE_FLAG,
constants.INPUT_ASCII)
else:
self.set_arg(fuzz_args, constants.INPUT_TYPE_FLAG,
constants.INPUT_BINARY)
if not environment.is_android():
# Attempt to start the fuzzer.
fuzz_result = self.run_and_wait(
additional_args=fuzz_args,
timeout=max_total_time,
terminate_before_kill=True,
terminate_wait_time=self.SIGTERM_WAIT_TIME,
)
else:
android_params = []
android_params = self.get_afl_environment_variables()
android_params.append(android.util.get_device_path(self.afl_fuzz_path))
# Attempt to start the fuzzer.
fuzz_result = self.run_and_wait(
additional_args=android_params + fuzz_args,
timeout=max_total_time,
terminate_before_kill=True,
terminate_wait_time=self.SIGTERM_WAIT_TIME,
)
# Reduce max_total_time by the amount of time the last attempt took.
max_total_time -= fuzz_result.time_executed
# Break now only if everything went well. Note that if afl finds a crash
# from fuzzing (and not in the input) it will exit with a zero return
# code.
if fuzz_result.return_code == 0:
# If afl-fuzz found a crash, copy it to the testcase_file_path.
self.afl_output.copy_crash_if_needed(self.testcase_file_path)
break
# Else the return_code was not 0 so something didn't work out. Try fixing
# this if afl-fuzz threw an error because it saw a crash, hang or large
# file in the starting corpus.
# If there was a crash in the input/corpus, afl-fuzz won't run, so let
# ClusterFuzz know about this and quit.
crash_filename = check_error_and_log(self.CRASH_REGEX,
self.CRASH_LOG_MESSAGE)
if crash_filename:
crash_path = os.path.join(self.afl_input.input_directory,
crash_filename)
# Copy this file over so afl can reproduce the crash.
shutil.copyfile(crash_path, self.testcase_file_path)
break
# afl-fuzz won't run if there is a hang in the input.
hang_filename = check_error_and_log(self.HANG_REGEX,
self.HANG_LOG_MESSAGE)
if hang_filename:
# Remove hang from queue and resume fuzzing
self.afl_output.remove_hang_in_queue(hang_filename)
# Now that the bad testcase has been removed, let's resume fuzzing so we
# don't start again from the beginning of the corpus.
self.set_resume(fuzz_args)
if hang_filename.startswith('id:000000'):
num_first_testcase_hangs += 1
if (num_first_testcase_hangs >
self.MAX_FIRST_HANGS_WITH_DEFERRED_FORKSERVER):
logs.log_warn('First testcase hangs when not deferring.')
elif (num_first_testcase_hangs ==
self.MAX_FIRST_HANGS_WITH_DEFERRED_FORKSERVER):
environment.set_value(constants.DONT_DEFER_ENV_VAR, 1)
print('Instructing AFL not to defer forkserver.\nIf this fixes the '
'fuzzer, you should add this to the .options file:\n'
'[env]\n'
'afl_driver_dont_defer = 1')
if num_retries - 1 > self.MAX_FUZZ_RETRIES_WITH_STRICT_TIMEOUT:
skip_hangs = True
self.set_timeout_arg(fuzz_args, self.MANUAL_TIMEOUT_MILLISECONDS,
skip_hangs)
continue
# If False: then prepare_retry_if_cpu_error can't solve the issue.
if self.prepare_retry_if_cpu_error(fuzz_result):
continue # Try fuzzing again with the cpu error fixed.
# If we can't do anything useful about the error, log it and don't try to
# fuzz again.
logs.log_error(
('Afl exited with a non-zero exitcode: %s. Cannot recover.' %
fuzz_result.return_code),
engine_output=fuzz_result.output)
break
return fuzz_result
def prepare_retry_if_cpu_error(self, fuzz_result):