├── .gitignore ├── .gitlab-ci.yml ├── .shellphuzz.ini ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── analyze_result.py ├── create_dict.py └── kernel_config.sh ├── fuzzer ├── __init__.py ├── extensions │ ├── __init__.py │ ├── extender.py │ └── grease_callback.py ├── fuzzer.py ├── hierarchy.py ├── minimizer.py └── showmap.py ├── reqs.txt ├── setup.py ├── shellphuzz └── tests └── test_fuzzer.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /dist/ 3 | /*.egg-info 4 | /.coverage 5 | /cover 6 | /bin/afl-cgc/ 7 | /bin/afl-unix/ 8 | /bin/fuzzer-libs/ 9 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - which angr-test && angr-test sync hard 3 | 4 | fuzzer: 5 | script: "angr-test fuzzer" 6 | tags: [angr] 7 | 8 | driller: 9 | script: "angr-test driller" 10 | tags: [angr] 11 | 12 | lint: 13 | script: "angr-test lint" 14 | tags: [angr] 15 | 16 | build_images: 17 | only: ["master"] 18 | stage: deploy 19 | script: 20 | - cgc-build worker 21 | tags: ["docker-builder"] 22 | -------------------------------------------------------------------------------- /.shellphuzz.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,fuzzer.fuzzer 3 | 4 | [logger_root] 5 | level=NOTSET 6 | handlers=hand01 7 | 8 | [logger_fuzzer.fuzzer] 9 | level=DEBUG 10 | handlers=hand01 11 | qualname=fuzzer.fuzzer 12 | 13 | [handlers] 14 | keys=hand01 15 | 16 | [handler_hand01] 17 | class=StreamHandler 18 | level=NOTSET 19 | formatter=form01 20 | args=(sys.stdout,) 21 | 22 | [formatters] 23 | keys=form01 24 | 25 | [formatter_form01] 26 | format=F1 %(asctime)s %(levelname)s %(message)s 27 | datefmt= 28 | class=logging.Formatter 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | env: 4 | - PY=e ANGR_REPO=fuzzer 5 | - PY=e ANGR_REPO=driller 6 | install: ( curl https://raw.githubusercontent.com/angr/angr-dev/$TRAVIS_BRANCH/tests/travis-install.sh | grep -v 404 || curl https://raw.githubusercontent.com/angr/angr-dev/master/tests/travis-install.sh ) | bash 7 | script: ~/angr-dev/tests/travis-test.sh 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, The Regents of the University of California 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuzzer 2 | 3 | This module provides a Python wrapper for interacting with AFL (American Fuzzy Lop: http://lcamtuf.coredump.cx/afl/). 4 | It supports starting an AFL instance, adding slave workers, injecting and retrieving testcases, and checking various performance metrics. 5 | Shellphish used it in Mechanical Phish (our CRS for the Cyber Grand Challenge) to interact with AFL. 6 | 7 | ## Installation 8 | 9 | /!\ We recommend installing our Python packages in a Python virtual environment. That is how we do it, and you'll likely run into problems if you do it otherwise. 10 | 11 | The fuzzer has some dependencies. 12 | First, here's a probably-incomplete list of debian packages that might be useful: 13 | 14 | sudo apt-get install build-essential gcc-multilib libtool automake autoconf bison debootstrap debian-archive-keyring libtool-bin 15 | sudo apt-get build-dep qemu 16 | 17 | Then, the fuzzer also depends on `shellphish-afl`, which is a pip package that actually includes AFL: 18 | 19 | pip install git+https://github.com/shellphish/shellphish-afl 20 | 21 | That'll pull a ton of other stuff, compile qemu about 4 times, and set everything up. 22 | Then, install this fuzzer wrapper: 23 | 24 | pip install git+https://github.com/shellphish/fuzzer 25 | 26 | ## Usage 27 | 28 | There are two ways of using this package. 29 | The easy way is to use the `shellphuzz` script, which allows you to specify various options, enable [driller](https://www.internetsociety.org/sites/default/files/blogs-media/driller-augmenting-fuzzing-through-selective-symbolic-execution.pdf), etc. 30 | The script has explanations about its usage with `--help`. 31 | 32 | A quick example: 33 | 34 | ``` 35 | # fuzz with 4 AFL cores 36 | shellphuzz -i -c 4 /path/to/binary 37 | 38 | # perform symbolic-assisted fuzzing with 4 AFL cores and 2 symbolic tracing (drilling) cores. 39 | shellphuzz -i -c 4 -d 2 /path/to/binary 40 | ``` 41 | 42 | You can also use it programmatically, but we have no documentation for that. 43 | For now, `import fuzzer` or look at the shellphuz script and figure it out ;-) 44 | -------------------------------------------------------------------------------- /bin/analyze_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import tqdm 6 | import json 7 | import fuzzer 8 | 9 | DIR = sys.argv[1].rstrip('/') 10 | BIN = os.path.basename(DIR).split('-')[-1] 11 | print(DIR,BIN) 12 | f = fuzzer.Fuzzer('/results/bins/%s'%BIN, '', job_dir=DIR) 13 | h = fuzzer.InputHierarchy(fuzzer=f, load_crashes=True) 14 | 15 | def good(_i): 16 | return _i.instance not in ('fuzzer-1', 'fuzzer-2', 'fuzzer-3', 'fuzzer-4', 'fuzzer-5') 17 | 18 | all_blocks = set() 19 | all_transitions = set() 20 | all_inputs = [ i for i in h.inputs.values() if not i.crash and good(i) ] 21 | all_crashes = [ i for i in h.inputs.values() if i.crash ] 22 | min_timestamp = min(i.timestamp for i in all_inputs) 23 | if all_crashes: 24 | first_crash = min(all_crashes, key=lambda i: i.timestamp) 25 | time_to_crash = first_crash.timestamp - min_timestamp 26 | first_crash_techniques = first_crash.contributing_techniques 27 | if 'grease' in first_crash_techniques : 28 | # TODO: figure out how long that input took 29 | time_to_crash += 120 30 | else: 31 | first_crash = None 32 | time_to_crash = -1 33 | first_crash_techniques = set() 34 | 35 | for i in tqdm.tqdm(all_inputs): 36 | all_blocks.update(i.block_set) 37 | all_transitions.update(i.transition_set) 38 | 39 | fuzzer_only = { i for i in all_inputs if list(i.contributing_techniques) == ['fuzzer'] } 40 | grease_derived = { i for i in all_inputs if 'grease' in i.contributing_techniques } 41 | driller_derived = { i for i in all_inputs if 'driller' in i.contributing_techniques } 42 | hybrid_derived = grease_derived & driller_derived 43 | #tc = h.technique_contributions() 44 | 45 | tag = ''.join(DIR.split('/')[-1].split('-')[:-2]) 46 | 47 | results = { 48 | 'bin': BIN, 49 | 'tag': tag, 50 | 'testcase_count': len(all_inputs), 51 | 'crash_count': len(all_crashes), 52 | 'crashed': len(all_crashes)>0, 53 | 'crash_time': time_to_crash, 54 | 'crash_techniques': tuple(first_crash_techniques), 55 | 'grease_assisted_crash': 'grease' in first_crash_techniques, 56 | 'driller_assisted_crash': 'driller' in first_crash_techniques, 57 | 'fuzzer_assisted_crash': 'fuzzer' in first_crash_techniques, 58 | 'fuzzer_only_testcases': len(fuzzer_only), 59 | 'greese_derived_testcases': len(grease_derived), 60 | 'driller_derived_testcases': len(driller_derived), 61 | 'hybrid_derived_testcases': len(hybrid_derived), 62 | 'blocks_triggered': len(all_blocks), 63 | 'transitions_triggered': len(all_transitions), 64 | } 65 | 66 | print("") 67 | for k,v in results.items(): 68 | print("RESULT", results['tag'], results['bin'], k, v) 69 | print("") 70 | print("JSON", json.dumps(results)) 71 | 72 | #print("RESULT",tag,BIN,": fuzzer blocks:",tc.get('fuzzer', (0,0))[0]) 73 | #print("RESULT",tag,BIN,": driller blocks:",tc.get('driller', (0,0))[0]) 74 | #print("RESULT",tag,BIN,": grease blocks:",tc.get('grease', (0,0))[0]) 75 | #print("RESULT",tag,BIN,": fuzzer crashes:",tc.get('fuzzer', (0,0))[1]) 76 | #print("RESULT",tag,BIN,": driller crashes:",tc.get('driller', (0,0))[1]) 77 | #print("RESULT",tag,BIN,": grease crashes:",tc.get('grease', (0,0))[1]) 78 | -------------------------------------------------------------------------------- /bin/create_dict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import angr 6 | import string 7 | import itertools 8 | 9 | import logging 10 | 11 | l = logging.getLogger("create_dict") 12 | 13 | 14 | def hexescape(s): 15 | ''' 16 | perform hex escaping on a raw string s 17 | ''' 18 | 19 | out = [] 20 | acceptable = (string.ascii_letters + string.digits + " .").encode() 21 | for c in s: 22 | if c not in acceptable: 23 | out.append("\\x%02x" % c) 24 | else: 25 | out.append(chr(c)) 26 | 27 | return ''.join(out) 28 | 29 | 30 | strcnt = itertools.count() 31 | 32 | def create(binary): 33 | 34 | b = angr.Project(binary, load_options={'auto_load_libs': False}) 35 | cfg = b.analyses.CFG(resolve_indirect_jumps=True, collect_data_references=True) 36 | 37 | state = b.factory.blank_state() 38 | 39 | string_references = [] 40 | for v in cfg._memory_data.values(): 41 | if v.sort == "string" and v.size > 1: 42 | st = state.solver.eval(state.memory.load(v.address, v.size), cast_to=bytes) 43 | string_references.append((v.address, st)) 44 | 45 | strings = [] if len(string_references) == 0 else list(list(zip(*string_references))[1]) 46 | 47 | valid_strings = [] 48 | if len(strings) > 0: 49 | for s in strings: 50 | if len(s) <= 128: 51 | valid_strings.append(s) 52 | for s_atom in s.split(): 53 | # AFL has a limit of 128 bytes per dictionary entries 54 | if len(s_atom) <= 128: 55 | valid_strings.append(s_atom) 56 | 57 | for s in set(valid_strings): 58 | s_val = hexescape(s) 59 | print("string_%d=\"%s\"" % (next(strcnt), s_val)) 60 | 61 | 62 | def main(argv): 63 | 64 | if len(argv) < 2: 65 | l.error("incorrect number of arguments passed to create_dict") 66 | print("usage: %s [binary1] [binary2] [binary3] ... " % sys.argv[0]) 67 | return 1 68 | 69 | for binary in argv[1:]: 70 | if os.path.isfile(binary): 71 | create(binary) 72 | 73 | return int(next(strcnt) == 0) 74 | 75 | if __name__ == "__main__": 76 | sys.exit(main(sys.argv)) 77 | -------------------------------------------------------------------------------- /bin/kernel_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | echo 1 | sudo tee /proc/sys/kernel/sched_child_runs_first 4 | echo core | sudo tee /proc/sys/kernel/core_pattern 5 | cd /sys/devices/system/cpu; echo performance | sudo tee cpu*/cpufreq/scaling_governor 6 | -------------------------------------------------------------------------------- /fuzzer/__init__.py: -------------------------------------------------------------------------------- 1 | from .fuzzer import Fuzzer 2 | from .minimizer import Minimizer 3 | from .showmap import Showmap 4 | from .extensions import * 5 | from .hierarchy import * 6 | -------------------------------------------------------------------------------- /fuzzer/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from .extender import Extender 2 | from .grease_callback import GreaseCallback 3 | -------------------------------------------------------------------------------- /fuzzer/extensions/extender.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import random 5 | import struct 6 | import tempfile 7 | import subprocess 8 | import shellphish_qemu 9 | 10 | from fuzzer.showmap import Showmap 11 | 12 | import logging 13 | l = logging.getLogger("fuzzer.extensions.Extender") 14 | 15 | class Extender(object): 16 | 17 | def __init__(self, binary, sync_dir): 18 | 19 | self.binary = binary 20 | self.sync_dir = sync_dir 21 | 22 | self.current_fuzzer = None 23 | 24 | self.crash_count = 0 25 | self.test_count = 0 26 | 27 | self.name = self.__class__.__name__.lower() 28 | 29 | directories = [os.path.join(self.sync_dir, self.name), 30 | os.path.join(self.sync_dir, self.name, "crashes"), 31 | os.path.join(self.sync_dir, self.name, "queue"), 32 | os.path.join(self.sync_dir, self.name, ".synced")] 33 | 34 | self.crash_bitmap = dict() 35 | 36 | for directory in directories: 37 | try: 38 | os.makedirs(directory) 39 | except OSError: 40 | continue 41 | 42 | l.debug("Fuzzer extension %s initialized", self.name) 43 | 44 | def _current_sync_count(self, fuzzer): 45 | """ 46 | Get the current number of inputs belonging to `fuzzer` which we've already mutated. 47 | """ 48 | 49 | sync_file = os.path.join(self.sync_dir, self.name, ".synced", fuzzer) 50 | if os.path.exists(sync_file): 51 | with open(sync_file, 'rb') as f: 52 | sc = struct.unpack(" self.crash_bitmap[i]: 170 | interesting = True 171 | self.crash_bitmap[i] = shownmap[i] 172 | 173 | return interesting 174 | 175 | @staticmethod 176 | def _interesting_test(shownmap, bitmap): 177 | 178 | for i in shownmap.keys(): 179 | if shownmap[i] > (ord(bitmap[i]) ^ 0xff): 180 | return True 181 | 182 | return False 183 | 184 | def _submit_test(self, test_input, bitmap): 185 | 186 | sm = Showmap(self.binary, test_input) 187 | shownmap = sm.showmap() 188 | 189 | if sm.causes_crash and self._interesting_crash(shownmap): 190 | self._new_crash(test_input) 191 | l.info("Found a new crash (length %d)", len(test_input)) 192 | elif not sm.causes_crash and self._interesting_test(shownmap, bitmap): 193 | self._new_test(test_input) 194 | l.info("Found an interesting new input (length %d)", len(test_input)) 195 | else: 196 | l.debug("Found a dud") 197 | 198 | @staticmethod 199 | def _new_mutation(payload, extend_amount): 200 | 201 | def random_string(n): 202 | return bytes(random.choice(list(range(1, 9)) + list(range(11, 256))) for _ in range(n)) 203 | 204 | np = payload + random_string(extend_amount + random.randint(0, 0x1000)) 205 | l.debug("New mutation of length %d", len(np)) 206 | 207 | return np 208 | 209 | def _mutate(self, r, bitmap): 210 | 211 | receive_counts = self._get_receive_counts(r) 212 | 213 | for numerator, denominator in receive_counts: 214 | if numerator != denominator: 215 | extend_by = denominator - numerator 216 | 217 | if extend_by > 1000000: 218 | l.warning("Amount to extend is greater than 1,000,000, refusing to perform extension") 219 | continue 220 | 221 | for _ in range(10): 222 | test_input = self._new_mutation(r, extend_by) 223 | self._submit_test(test_input, bitmap) 224 | 225 | def _do_round(self): 226 | """ 227 | Single round of extending mutations. 228 | """ 229 | 230 | def _extract_number(iname): 231 | attrs = dict(map(lambda x: (x[0], x[-1]), map(lambda y: y.split(":"), iname.split(",")))) 232 | if "id" in attrs: 233 | return int(attrs["id"]) 234 | return 0 235 | 236 | for fuzzer in os.listdir(self.sync_dir): 237 | if fuzzer == self.name: 238 | continue 239 | l.debug("Looking to extend inputs in fuzzer '%s'", fuzzer) 240 | 241 | self.current_fuzzer = fuzzer 242 | synced = self._current_sync_count(fuzzer) 243 | c_synced = self._current_crash_sync_count(fuzzer) 244 | 245 | l.debug("Already worked on %d inputs from fuzzer '%s'", synced, fuzzer) 246 | 247 | bitmap = self._current_bitmap(fuzzer) 248 | 249 | # no bitmap, fuzzer probably hasn't started 250 | if bitmap is None: 251 | l.warning("No bitmap for fuzzer '%s', skipping", fuzzer) 252 | continue 253 | 254 | queue_dir = os.path.join(self.sync_dir, fuzzer, "queue") 255 | 256 | queue_l = [n for n in os.listdir(queue_dir) if n != '.state'] 257 | new_q = [i for i in queue_l if _extract_number(i) >= synced] 258 | 259 | crash_dir = os.path.join(self.sync_dir, fuzzer, "crashes") 260 | crash_l = [n for n in os.listdir(crash_dir) if n != 'README.txt'] 261 | new_c = [i for i in crash_l if _extract_number(i) >= c_synced] 262 | new = new_q + new_c 263 | if len(new): 264 | l.info("Found %d new inputs to extend", len(new)) 265 | 266 | for ninput in new_q: 267 | n_path = os.path.join(queue_dir, ninput) 268 | with open(n_path, "rb") as f: 269 | self._mutate(f.read(), bitmap) 270 | 271 | for ninput in new_c: 272 | n_path = os.path.join(crash_dir, ninput) 273 | with open(n_path, "rb") as f: 274 | self._mutate(f.read(), bitmap) 275 | 276 | self._update_sync_count(fuzzer, len(queue_l)) 277 | self._update_crash_sync_count(fuzzer, len(crash_l)) 278 | 279 | def run(self): 280 | 281 | while True: 282 | self._do_round() 283 | time.sleep(3) 284 | 285 | if __name__ == "__main__": 286 | l.setLevel("INFO") 287 | 288 | if len(sys.argv) > 2: 289 | b = sys.argv[1] 290 | s = sys.argv[2] 291 | 292 | e = Extender(b, s) 293 | e.run() 294 | else: 295 | sys.exit(1) 296 | -------------------------------------------------------------------------------- /fuzzer/extensions/grease_callback.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import logging 4 | from fuzzer import Showmap 5 | 6 | l = logging.getLogger("grease_callback") 7 | 8 | class GreaseCallback(object): 9 | def __init__(self, grease_dir, grease_filter=None, grease_sorter=None): 10 | self._grease_dir = grease_dir 11 | assert os.path.exists(grease_dir) 12 | self._grease_filter = grease_filter if grease_filter is not None else lambda x: True 13 | self._grease_sorter = grease_sorter if grease_sorter is not None else lambda x: x 14 | 15 | def grease_callback(self, fuzz): 16 | l.warning("we are stuck, trying to grease the wheels!") 17 | 18 | # find an unused input 19 | grease_inputs = [ 20 | os.path.join(self._grease_dir, x) for x in os.listdir(self._grease_dir) 21 | if self._grease_filter(os.path.join(self._grease_dir, x)) 22 | ] 23 | 24 | if len(grease_inputs) == 0: 25 | l.warning("no grease inputs remaining") 26 | return 27 | 28 | # iterate until we find one with a new bitmap 29 | bitmap = fuzz.bitmap() 30 | for a in self._grease_sorter(grease_inputs): 31 | if os.path.getsize(a) == 0: 32 | continue 33 | with open(a) as sf: 34 | seed_content = sf.read() 35 | smap = Showmap(fuzz.binary_path, seed_content) 36 | shownmap = smap.showmap() 37 | for k in shownmap: 38 | #print(shownmap[k], (ord(bitmap[k % len(bitmap)]) ^ 0xff)) 39 | if shownmap[k] > (ord(bitmap[k % len(bitmap)]) ^ 0xff): 40 | l.warning("Found interesting, syncing to tests") 41 | 42 | fuzzer_out_dir = fuzz.out_dir 43 | grease_dir = os.path.join(fuzzer_out_dir, "grease") 44 | grease_queue_dir = os.path.join(grease_dir, "queue") 45 | try: 46 | os.mkdir(grease_dir) 47 | os.mkdir(grease_queue_dir) 48 | except OSError: 49 | pass 50 | id_num = len(os.listdir(grease_queue_dir)) 51 | filepath = "id:" + ("%d" % id_num).rjust(6, "0") + ",grease" 52 | filepath = os.path.join(grease_queue_dir, filepath) 53 | shutil.copy(a, filepath) 54 | l.warning("copied grease input: %s", os.path.basename(a)) 55 | return 56 | 57 | l.warning("No interesting inputs found") 58 | __call__ = grease_callback 59 | -------------------------------------------------------------------------------- /fuzzer/fuzzer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import angr 5 | import signal 6 | import shutil 7 | import threading 8 | import subprocess 9 | import shellphish_afl 10 | 11 | import logging 12 | 13 | l = logging.getLogger("fuzzer.fuzzer") 14 | 15 | config = { } 16 | 17 | class InstallError(Exception): 18 | pass 19 | 20 | 21 | # http://stackoverflow.com/a/41450617 22 | class InfiniteTimer(): 23 | """A Timer class that does not stop, unless you want it to.""" 24 | 25 | def __init__(self, seconds, target): 26 | self._should_continue = False 27 | self.is_running = False 28 | self.seconds = seconds 29 | self.target = target 30 | self.thread = None 31 | 32 | def _handle_target(self): 33 | self.is_running = True 34 | self.target() 35 | self.is_running = False 36 | self._start_timer() 37 | 38 | def _start_timer(self): 39 | if self._should_continue: # Code could have been running when cancel was called. 40 | self.thread = threading.Timer(self.seconds, self._handle_target) 41 | self.thread.start() 42 | 43 | def start(self): 44 | if not self._should_continue and not self.is_running: 45 | self._should_continue = True 46 | self._start_timer() 47 | else: 48 | print("Timer already started or running, please wait if you're restarting.") 49 | 50 | def cancel(self): 51 | if self.thread is not None: 52 | self._should_continue = False # Just in case thread is running and cancel fails. 53 | self.thread.cancel() 54 | else: 55 | pass 56 | #print("Timer never started or failed to initialize.") 57 | 58 | 59 | 60 | class Fuzzer(object): 61 | ''' Fuzzer object, spins up a fuzzing job on a binary ''' 62 | 63 | def __init__( 64 | self, binary_path, work_dir, afl_count=1, library_path=None, time_limit=None, memory="8G", 65 | target_opts=None, extra_opts=None, create_dictionary=False, 66 | seeds=None, crash_mode=False, never_resume=False, qemu=True, stuck_callback=None, 67 | force_interval=None, job_dir=None, timeout=None 68 | ): 69 | ''' 70 | :param binary_path: path to the binary to fuzz. List or tuple for multi-CB. 71 | :param work_dir: the work directory which contains fuzzing jobs, our job directory will go here 72 | :param afl_count: number of AFL jobs total to spin up for the binary 73 | :param library_path: library path to use, if none is specified a default is chosen 74 | :param timelimit: amount of time to fuzz for, has no effect besides returning True when calling timed_out 75 | :param seeds: list of inputs to seed fuzzing with 76 | :param target_opts: extra options to pass to the target 77 | :param extra_opts: extra options to pass to AFL when starting up 78 | :param crash_mode: if set to True AFL is set to crash explorer mode, and seed will be expected to be a crashing input 79 | :param never_resume: never resume an old fuzzing run, even if it's possible 80 | :param qemu: Utilize QEMU for instrumentation of binary. 81 | :param memory: AFL child process memory limit (default: "8G") 82 | :param stuck_callback: the callback to call when afl has no pending fav's 83 | :param job_dir: a job directory to override the work_dir/binary_name path 84 | :param timeout: timeout for individual runs within AFL 85 | ''' 86 | 87 | self.binary_path = binary_path 88 | self.work_dir = work_dir 89 | self.afl_count = afl_count 90 | self.time_limit = time_limit 91 | self.library_path = library_path 92 | self.target_opts = [ ] if target_opts is None else target_opts 93 | self.crash_mode = crash_mode 94 | self.memory = memory 95 | self.qemu = qemu 96 | self.force_interval = force_interval 97 | self.timeout = timeout 98 | 99 | Fuzzer._perform_env_checks() 100 | 101 | if isinstance(binary_path, str): 102 | self.is_multicb = False 103 | self.binary_id = os.path.basename(binary_path) 104 | elif isinstance(binary_path,(list,tuple)): 105 | self.is_multicb = True 106 | self.binary_id = os.path.basename(binary_path[0]) 107 | else: 108 | raise ValueError("Was expecting either a string or a list/tuple for binary_path! It's {} instead.".format(type(binary_path))) 109 | 110 | # sanity check crash mode 111 | if self.crash_mode: 112 | if seeds is None: 113 | raise ValueError("Seeds must be specified if using the fuzzer in crash mode") 114 | l.info("AFL will be started in crash mode") 115 | 116 | self.seeds = [b"fuzz"] if seeds is None or len(seeds) == 0 else seeds 117 | 118 | self.job_dir = os.path.join(self.work_dir, self.binary_id) if not job_dir else job_dir 119 | self.in_dir = os.path.join(self.job_dir, "input") 120 | self.out_dir = os.path.join(self.job_dir, "sync") 121 | 122 | # sanity check extra opts 123 | self.extra_opts = extra_opts 124 | if self.extra_opts is not None: 125 | if not isinstance(self.extra_opts, list): 126 | raise ValueError("extra_opts must be a list of command line arguments") 127 | 128 | # base of the fuzzer package 129 | self.base = Fuzzer._get_base() 130 | 131 | self.start_time = int(time.time()) 132 | # create_dict script 133 | self.create_dict_path = os.path.join(self.base, "bin", "create_dict.py") 134 | # afl dictionary 135 | self.dictionary = None 136 | # processes spun up 137 | self.procs = [ ] 138 | # start the fuzzer ids at 0 139 | self.fuzz_id = 0 140 | # test if we're resuming an old run 141 | self.resuming = bool(os.listdir(self.out_dir)) if os.path.isdir(self.out_dir) else False 142 | # has the fuzzer been turned on? 143 | self._on = False 144 | 145 | if never_resume and self.resuming: 146 | l.info("could resume, but starting over upon request") 147 | shutil.rmtree(self.job_dir) 148 | self.resuming = False 149 | 150 | if self.is_multicb: 151 | # Where cgc/setup's Dockerfile checks it out 152 | # NOTE: 'afl/fakeforksrv' serves as 'qemu', as far as AFL is concerned 153 | # Will actually invoke 'fakeforksrv/multicb-qemu' 154 | # This QEMU cannot run standalone (always speaks the forkserver "protocol"), 155 | # but 'fakeforksrv/run_via_fakeforksrv' allows it. 156 | # XXX: There is no driller/angr support, and probably will never be. 157 | self.afl_path = shellphish_afl.afl_bin('multi-cgc') 158 | self.afl_path_var = shellphish_afl.afl_path_var('multi-cgc') 159 | self.qemu_name = 'TODO' 160 | else: 161 | 162 | p = angr.Project(binary_path) 163 | 164 | self.os = p.loader.main_object.os 165 | 166 | self.afl_dir = shellphish_afl.afl_dir(self.os) 167 | 168 | # the path to AFL capable of calling driller 169 | self.afl_path = shellphish_afl.afl_bin(self.os) 170 | 171 | if self.os == 'cgc': 172 | self.afl_path_var = shellphish_afl.afl_path_var('cgc') 173 | else: 174 | self.afl_path_var = shellphish_afl.afl_path_var(p.arch.qemu_name) 175 | # set up libraries 176 | self._export_library_path(p) 177 | 178 | # the name of the qemu port used to run these binaries 179 | self.qemu_name = p.arch.qemu_name 180 | 181 | self.qemu_dir = self.afl_path_var 182 | 183 | l.debug("self.start_time: %r", self.start_time) 184 | l.debug("self.afl_path: %s", self.afl_path) 185 | l.debug("self.afl_path_var: %s", self.afl_path_var) 186 | l.debug("self.qemu_dir: %s", self.qemu_dir) 187 | l.debug("self.binary_id: %s", self.binary_id) 188 | l.debug("self.work_dir: %s", self.work_dir) 189 | l.debug("self.resuming: %s", self.resuming) 190 | 191 | # if we're resuming an old run set the input_directory to a '-' 192 | if self.resuming: 193 | l.info("[%s] resuming old fuzzing run", self.binary_id) 194 | self.in_dir = "-" 195 | 196 | else: 197 | # create the work directory and input directory 198 | try: 199 | os.makedirs(self.in_dir) 200 | except OSError: 201 | l.warning("unable to create in_dir \"%s\"", self.in_dir) 202 | 203 | # populate the input directory 204 | self._initialize_seeds() 205 | 206 | # look for a dictionary 207 | dictionary_file = os.path.join(self.job_dir, "%s.dict" % self.binary_id) 208 | if os.path.isfile(dictionary_file): 209 | self.dictionary = dictionary_file 210 | 211 | # if a dictionary doesn't exist and we aren't resuming a run, create a dict 212 | elif not self.resuming: 213 | # call out to another process to create the dictionary so we can 214 | # limit it's memory 215 | if create_dictionary: 216 | if self._create_dict(dictionary_file): 217 | self.dictionary = dictionary_file 218 | l.warning("done making dictionary") 219 | else: 220 | # no luck creating a dictionary 221 | l.warning("[%s] unable to create dictionary", self.binary_id) 222 | 223 | if self.force_interval is None: 224 | l.warning("not forced") 225 | self._timer = InfiniteTimer(30, self._timer_callback) 226 | else: 227 | l.warning("forced") 228 | self._timer = InfiniteTimer(self.force_interval, self._timer_callback) 229 | 230 | self._stuck_callback = stuck_callback 231 | 232 | # set environment variable for the AFL_PATH 233 | os.environ['AFL_PATH'] = self.afl_path_var 234 | 235 | ### EXPOSED 236 | def start(self): 237 | ''' 238 | start fuzzing 239 | ''' 240 | 241 | # spin up the AFL workers 242 | self._start_afl() 243 | 244 | # start the callback timer 245 | self._timer.start() 246 | 247 | self._on = True 248 | 249 | @property 250 | def alive(self): 251 | if not self._on or not len(self.stats): 252 | return False 253 | 254 | alive_cnt = 0 255 | if self._on: 256 | for fuzzer in self.stats: 257 | try: 258 | os.kill(int(self.stats[fuzzer]['fuzzer_pid']), 0) 259 | alive_cnt += 1 260 | except (OSError, KeyError): 261 | pass 262 | 263 | return bool(alive_cnt) 264 | 265 | def kill(self): 266 | for p in self.procs: 267 | p.terminate() 268 | p.wait() 269 | 270 | if hasattr(self, "_timer"): 271 | self._timer.cancel() 272 | 273 | self._on = False 274 | 275 | @property 276 | def stats(self): 277 | 278 | # collect stats into dictionary 279 | stats = {} 280 | if os.path.isdir(self.out_dir): 281 | for fuzzer_dir in os.listdir(self.out_dir): 282 | stat_path = os.path.join(self.out_dir, fuzzer_dir, "fuzzer_stats") 283 | if os.path.isfile(stat_path): 284 | stats[fuzzer_dir] = {} 285 | 286 | with open(stat_path, "r") as f: 287 | stat_blob = f.read() 288 | stat_lines = stat_blob.split("\n")[:-1] 289 | for stat in stat_lines: 290 | key, val = stat.split(":") 291 | stats[fuzzer_dir][key.strip()] = val.strip() 292 | 293 | return stats 294 | 295 | def found_crash(self): 296 | 297 | return len(self.crashes()) > 0 298 | 299 | def add_fuzzer(self): 300 | ''' 301 | add one fuzzer 302 | ''' 303 | 304 | self.procs.append(self._start_afl_instance()) 305 | 306 | def add_extension(self, name): 307 | """ 308 | Spawn the mutation extension `name` 309 | :param name: name of extension 310 | :returns: True if able to spawn extension 311 | """ 312 | 313 | extension_path = os.path.join(os.path.dirname(__file__), "..", "fuzzer", "extensions", "%s.py" % name) 314 | rpath = os.path.realpath(extension_path) 315 | 316 | l.debug("Attempting to spin up extension %s", rpath) 317 | 318 | if os.path.exists(extension_path): 319 | args = [sys.executable, extension_path, self.binary_path, self.out_dir] 320 | 321 | outfile_leaf = "%s-%d.log" % (name, len(self.procs)) 322 | outfile = os.path.join(self.job_dir, outfile_leaf) 323 | with open(outfile, "wb") as fp: 324 | p = subprocess.Popen(args, stderr=fp) 325 | self.procs.append(p) 326 | return True 327 | 328 | return False 329 | 330 | def add_fuzzers(self, n): 331 | for _ in range(n): 332 | self.add_fuzzer() 333 | 334 | def remove_fuzzer(self): 335 | ''' 336 | remove one fuzzer 337 | ''' 338 | 339 | try: 340 | f = self.procs.pop() 341 | except IndexError: 342 | l.error("no fuzzer to remove") 343 | raise ValueError("no fuzzer to remove") 344 | 345 | f.kill() 346 | 347 | def remove_fuzzers(self, n): 348 | ''' 349 | remove multiple fuzzers 350 | ''' 351 | 352 | if n > len(self.procs): 353 | l.error("not more than %u fuzzers to remove", n) 354 | raise ValueError("not more than %u fuzzers to remove" % n) 355 | 356 | if n == len(self.procs): 357 | l.warning("removing all fuzzers") 358 | 359 | for _ in range(n): 360 | self.remove_fuzzer() 361 | 362 | def _get_crashing_inputs(self, signals): 363 | """ 364 | Retrieve the crashes discovered by AFL. Only return those crashes which 365 | recieved a signal within 'signals' as the kill signal. 366 | 367 | :param signals: list of valid kill signal numbers 368 | :return: a list of strings which are crashing inputs 369 | """ 370 | 371 | crashes = set() 372 | for fuzzer in os.listdir(self.out_dir): 373 | crashes_dir = os.path.join(self.out_dir, fuzzer, "crashes") 374 | 375 | if not os.path.isdir(crashes_dir): 376 | # if this entry doesn't have a crashes directory, just skip it 377 | continue 378 | 379 | for crash in os.listdir(crashes_dir): 380 | if crash == "README.txt": 381 | # skip the readme entry 382 | continue 383 | 384 | attrs = dict(map(lambda x: (x[0], x[-1]), map(lambda y: y.split(":"), crash.split(",")))) 385 | 386 | if int(attrs['sig']) not in signals: 387 | continue 388 | 389 | crash_path = os.path.join(crashes_dir, crash) 390 | with open(crash_path, 'rb') as f: 391 | crashes.add(f.read()) 392 | 393 | return list(crashes) 394 | 395 | def crashes(self, signals=(signal.SIGSEGV, signal.SIGILL)): 396 | """ 397 | Retrieve the crashes discovered by AFL. Since we are now detecting flag 398 | page leaks (via SIGUSR1) we will not return these leaks as crashes. 399 | Instead, these 'crashes' can be found with the leaks function. 400 | 401 | :param signals: list of valid kill signal numbers to override the default (SIGSEGV and SIGILL) 402 | :return: a list of strings which are crashing inputs 403 | """ 404 | 405 | return self._get_crashing_inputs(signals) 406 | 407 | def queue(self, fuzzer='fuzzer-master'): 408 | ''' 409 | retrieve the current queue of inputs from a fuzzer 410 | :return: a list of strings which represent a fuzzer's queue 411 | ''' 412 | 413 | if not fuzzer in os.listdir(self.out_dir): 414 | raise ValueError("fuzzer '%s' does not exist" % fuzzer) 415 | 416 | queue_path = os.path.join(self.out_dir, fuzzer, 'queue') 417 | queue_files = list(filter(lambda x: x != ".state", os.listdir(queue_path))) 418 | 419 | queue_l = [ ] 420 | for q in queue_files: 421 | with open(os.path.join(queue_path, q), 'rb') as f: 422 | queue_l.append(f.read()) 423 | 424 | return queue_l 425 | 426 | def bitmap(self, fuzzer='fuzzer-master'): 427 | ''' 428 | retrieve the bitmap for the fuzzer `fuzzer`. 429 | :return: a string containing the contents of the bitmap. 430 | ''' 431 | 432 | if not fuzzer in os.listdir(self.out_dir): 433 | raise ValueError("fuzzer '%s' does not exist" % fuzzer) 434 | 435 | bitmap_path = os.path.join(self.out_dir, fuzzer, "fuzz_bitmap") 436 | 437 | bdata = None 438 | try: 439 | with open(bitmap_path, "rb") as f: 440 | bdata = f.read() 441 | except IOError: 442 | pass 443 | 444 | return bdata 445 | 446 | def timed_out(self): 447 | if self.time_limit is None: 448 | return False 449 | return time.time() - self.start_time > self.time_limit 450 | 451 | def pollenate(self, testcases): 452 | ''' 453 | pollenate a fuzzing job with new testcases 454 | 455 | :param testcases: list of bytes objects representing new inputs to introduce 456 | ''' 457 | 458 | nectary_queue_directory = os.path.join(self.out_dir, 'pollen', 'queue') 459 | if not 'pollen' in os.listdir(self.out_dir): 460 | os.makedirs(nectary_queue_directory) 461 | 462 | pollen_cnt = len(os.listdir(nectary_queue_directory)) 463 | 464 | for tcase in testcases: 465 | with open(os.path.join(nectary_queue_directory, "id:%06d,src:pollenation" % pollen_cnt), "wb") as f: 466 | f.write(tcase) 467 | 468 | pollen_cnt += 1 469 | 470 | ### FUZZ PREP 471 | 472 | def _initialize_seeds(self): 473 | ''' 474 | populate the input directory with the seeds specified 475 | ''' 476 | 477 | assert len(self.seeds) > 0, "Must specify at least one seed to start fuzzing with" 478 | 479 | l.debug("initializing seeds %r", self.seeds) 480 | 481 | template = os.path.join(self.in_dir, "seed-%d") 482 | for i, seed in enumerate(self.seeds): 483 | with open(template % i, "wb") as f: 484 | f.write(seed) 485 | 486 | ### DICTIONARY CREATION 487 | 488 | def _create_dict(self, dict_file): 489 | 490 | l.warning("creating a dictionary of string references within binary \"%s\"", 491 | self.binary_id) 492 | 493 | args = [sys.executable, self.create_dict_path] 494 | args += self.binary_path if self.is_multicb else [self.binary_path] 495 | 496 | with open(dict_file, "wb") as dfp: 497 | p = subprocess.Popen(args, stdout=dfp) 498 | retcode = p.wait() 499 | 500 | return retcode == 0 and os.path.getsize(dict_file) 501 | 502 | ### AFL SPAWNERS 503 | 504 | def _start_afl_instance(self): 505 | 506 | args = [self.afl_path] 507 | 508 | args += ["-i", self.in_dir] 509 | args += ["-o", self.out_dir] 510 | args += ["-m", self.memory] 511 | 512 | if self.qemu: 513 | args += ["-Q"] 514 | 515 | if self.crash_mode: 516 | args += ["-C"] 517 | 518 | if self.fuzz_id == 0: 519 | args += ["-M", "fuzzer-master"] 520 | outfile = "fuzzer-master.log" 521 | else: 522 | args += ["-S", "fuzzer-%d" % self.fuzz_id] 523 | outfile = "fuzzer-%d.log" % self.fuzz_id 524 | 525 | if self.dictionary is not None: 526 | args += ["-x", self.dictionary] 527 | 528 | if self.extra_opts is not None: 529 | args += self.extra_opts 530 | 531 | # auto-calculate timeout based on the number of binaries 532 | if self.is_multicb: 533 | args += ["-t", "%d+" % (1000 * len(self.binary_path))] 534 | elif self.timeout: 535 | args += ["-t", "%d+" % self.timeout] 536 | 537 | args += ["--"] 538 | args += self.binary_path if self.is_multicb else [self.binary_path] 539 | 540 | args.extend(self.target_opts) 541 | 542 | l.debug("execing: %s > %s", ' '.join(args), outfile) 543 | 544 | # increment the fuzzer ID 545 | self.fuzz_id += 1 546 | 547 | outfile = os.path.join(self.job_dir, outfile) 548 | with open(outfile, "w") as fp: 549 | return subprocess.Popen(args, stdout=fp, close_fds=True) 550 | 551 | def _start_afl(self): 552 | ''' 553 | start up a number of AFL instances to begin fuzzing 554 | ''' 555 | 556 | # spin up the master AFL instance 557 | master = self._start_afl_instance() # the master fuzzer 558 | self.procs.append(master) 559 | 560 | if self.afl_count > 1: 561 | driller = self._start_afl_instance() 562 | self.procs.append(driller) 563 | 564 | # only spins up an AFL instances if afl_count > 1 565 | for _ in range(2, self.afl_count): 566 | slave = self._start_afl_instance() 567 | self.procs.append(slave) 568 | 569 | ### UTIL 570 | 571 | @staticmethod 572 | def _perform_env_checks(): 573 | err = "" 574 | 575 | # check for afl sensitive settings 576 | with open("/proc/sys/kernel/core_pattern") as f: 577 | if not "core" in f.read(): 578 | err += "AFL Error: Pipe at the beginning of core_pattern\n" 579 | err += "execute 'echo core | sudo tee /proc/sys/kernel/core_pattern'\n" 580 | 581 | # This file is based on a driver not all systems use 582 | # http://unix.stackexchange.com/questions/153693/cant-use-userspace-cpufreq-governor-and-set-cpu-frequency 583 | # TODO: Perform similar performance check for other default drivers. 584 | if os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"): 585 | with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") as f: 586 | if not "performance" in f.read(): 587 | err += "AFL Error: Suboptimal CPU scaling governor\n" 588 | err += "execute 'cd /sys/devices/system/cpu; echo performance | sudo tee cpu*/cpufreq/scaling_governor'\n" 589 | 590 | # TODO: test, to be sure it doesn't mess things up 591 | with open("/proc/sys/kernel/sched_child_runs_first") as f: 592 | if not "1" in f.read(): 593 | err += "AFL Warning: We probably want the fork() children to run first\n" 594 | err += "execute 'echo 1 | sudo tee /proc/sys/kernel/sched_child_runs_first'\n" 595 | 596 | # Spit out all errors at the same time 597 | if err != "": 598 | l.error(err) 599 | raise InstallError(err) 600 | 601 | 602 | @staticmethod 603 | def _get_base(): 604 | ''' 605 | find the directory containing bin, there should always be a directory 606 | containing bin below base intially 607 | ''' 608 | base = os.path.dirname(__file__) 609 | 610 | while not "bin" in os.listdir(base) and os.path.abspath(base) != "/": 611 | base = os.path.join(base, "..") 612 | 613 | if os.path.abspath(base) == "/": 614 | raise InstallError("could not find afl install directory") 615 | 616 | return base 617 | 618 | def _export_library_path(self, p): 619 | ''' 620 | export the correct library path for a given architecture 621 | ''' 622 | path = None 623 | 624 | if self.library_path is None: 625 | directory = None 626 | if p.arch.qemu_name == "aarch64": 627 | directory = "arm64" 628 | if p.arch.qemu_name == "i386": 629 | directory = "i386" 630 | if p.arch.qemu_name == "x86_64": 631 | directory = "x86_64" 632 | if p.arch.qemu_name == "mips": 633 | directory = "mips" 634 | if p.arch.qemu_name == "mipsel": 635 | directory = "mipsel" 636 | if p.arch.qemu_name == "ppc": 637 | directory = "powerpc" 638 | if p.arch.qemu_name == "arm": 639 | # some stuff qira uses to determine the which libs to use for arm 640 | with open(self.binary_path, "rb") as f: progdata = f.read(0x800) 641 | if "/lib/ld-linux.so.3" in progdata: 642 | directory = "armel" 643 | elif "/lib/ld-linux-armhf.so.3" in progdata: 644 | directory = "armhf" 645 | 646 | if directory is None: 647 | l.warning("architecture \"%s\" has no installed libraries", p.arch.qemu_name) 648 | else: 649 | path = os.path.join(self.afl_dir, "..", "fuzzer-libs", directory) 650 | else: 651 | path = self.library_path 652 | 653 | if path is not None: 654 | l.debug("exporting QEMU_LD_PREFIX of '%s'", path) 655 | os.environ['QEMU_LD_PREFIX'] = path 656 | 657 | def _timer_callback(self): 658 | if self._stuck_callback is not None: 659 | # check if afl has pending fav's 660 | if ('fuzzer-master' in self.stats and 'pending_favs' in self.stats['fuzzer-master'] and \ 661 | int(self.stats['fuzzer-master']['pending_favs']) == 0) or self.force_interval is not None: 662 | self._stuck_callback(self) 663 | 664 | def __del__(self): 665 | self.kill() 666 | -------------------------------------------------------------------------------- /fuzzer/hierarchy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import tqdm 4 | import glob 5 | import logging 6 | import networkx 7 | import subprocess 8 | import shellphish_qemu 9 | 10 | l = logging.getLogger('fuzzer.input_hierarchy') 11 | 12 | class Input(object): 13 | def __init__(self, filename, instance, hierarchy): 14 | self.hierarchy = hierarchy 15 | self.instance = instance 16 | self.filename = filename 17 | 18 | self.id = None 19 | self.source_ids = [ ] 20 | self.sources = [ ] 21 | self.cov = False 22 | self.op = None 23 | self.synced_from = None 24 | self.other_fields = { } 25 | self.val = None 26 | self.rep = None 27 | self.pos = None 28 | self.orig = None 29 | self.crash = False 30 | self.sig = None 31 | self._process_filename(filename) 32 | 33 | self.looped = False 34 | self.timestamp = os.stat(self.filepath).st_mtime 35 | 36 | # cached stuff 37 | self._trace = None 38 | self._origins = None 39 | self._contributing_techniques = None 40 | self._technique_contributions = None 41 | 42 | def _process_filename(self, filename): 43 | # process the fields 44 | fields = filename.split(',') 45 | for f in fields: 46 | if f == "+cov": 47 | self.cov = True 48 | elif f == "grease": 49 | assert self.id 50 | self.orig = "greased_%s" % self.id 51 | else: 52 | n,v = f.split(':', 1) 53 | if n == 'id': 54 | assert not self.id 55 | self.id = v 56 | elif n == 'src': 57 | assert not self.source_ids 58 | self.source_ids = v.split('+') 59 | elif n == 'sync': 60 | assert not self.synced_from 61 | self.synced_from = v 62 | elif n == 'op': 63 | assert not self.op 64 | self.op = v 65 | elif n == 'rep': 66 | assert not self.rep 67 | self.rep = v 68 | elif n == 'orig': 69 | assert not self.orig 70 | self.orig = v 71 | elif n == 'pos': 72 | assert not self.pos 73 | self.pos = v 74 | elif n == 'val': 75 | assert not self.val 76 | self.val = v 77 | elif n == 'from': # driller uses this instead of synced/src 78 | instance, from_id = v[:-6], v[-6:] 79 | self.synced_from = instance 80 | self.source_ids.append(from_id) 81 | elif n == 'sig': 82 | assert not self.crash 83 | assert not self.sig 84 | assert self.id 85 | self.crash = True 86 | self.sig = v 87 | self.id = 'c'+self.id 88 | else: 89 | l.warning("Got unexpected field %s with value %s for file %s.", n, v, filename) 90 | self.other_fields[n] = v 91 | 92 | assert self.id is not None 93 | assert self.source_ids or self.orig 94 | 95 | def _resolve_sources(self): 96 | try: 97 | if self.synced_from: 98 | self.sources = [ self.hierarchy.instance_input(self.synced_from, self.source_ids[0]) ] 99 | else: 100 | self.sources = [ self.hierarchy.instance_input(self.instance, i) for i in self.source_ids ] 101 | except KeyError as e: 102 | l.warning("Unable to resolve source ID %s for %s", e, self) 103 | self.sources = [ ] 104 | 105 | @property 106 | def filepath(self): 107 | return os.path.join( 108 | self.hierarchy._dir, self.instance, 109 | 'crashes' if self.crash else 'queue', self.filename 110 | ) 111 | 112 | def read(self): 113 | with open(self.filepath, 'rb') as f: 114 | return f.read() 115 | 116 | def __repr__(self): 117 | s = "" % (self.instance, self.filename) 118 | #if self.synced_from: 119 | # s += " sync:%s" % self.synced_from 120 | #s += "src:%s" % self.source_ids 121 | return s 122 | 123 | # 124 | # Lineage analysis. 125 | # 126 | 127 | @property 128 | def lineage(self): 129 | for p in self.sources: 130 | for pl in p.lineage: 131 | yield pl 132 | yield self 133 | 134 | def print_lineage(self, depth=0): 135 | if depth: 136 | print(' '*depth + str(self)) 137 | else: 138 | print(self) 139 | for parent in self.sources: 140 | parent.print_lineage(depth=depth+1) 141 | 142 | @property 143 | def origins(self, follow_extensions=False): 144 | """ 145 | Return the origins of this seed. 146 | """ 147 | if self._origins is not None: 148 | return self._origins 149 | 150 | if not follow_extensions and not self.instance.startswith('fuzzer-'): 151 | o = { self } 152 | elif not self.sources: 153 | o = { self } 154 | else: 155 | o = set.union(*(s.origins for s in self.sources)) 156 | self._origins = o 157 | return self._origins 158 | 159 | @property 160 | def technique(self): 161 | return 'fuzzer' if self.instance.startswith('fuzzer-') else self.instance 162 | 163 | @property 164 | def contributing_techniques(self): 165 | if self._contributing_techniques is None: 166 | # don't count this current technique if we synced it 167 | if self.synced_from: 168 | new_technique = frozenset() 169 | else: 170 | new_technique = frozenset([self.technique]) 171 | self._contributing_techniques = frozenset.union( 172 | new_technique, *(i.contributing_techniques for i in self.sources) 173 | ) 174 | return self._contributing_techniques 175 | 176 | @property 177 | def contributing_instances(self): 178 | return set(i.instance for i in self.lineage) 179 | 180 | @property 181 | def output(self): 182 | with open('/dev/null', 'w') as tf, open(self.filepath) as sf: 183 | cmd_args = [ 184 | 'timeout', '60', shellphish_qemu.qemu_path('cgc-tracer'), 185 | self.hierarchy._fuzzer.binary_path 186 | ] 187 | process = subprocess.Popen(cmd_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=tf) 188 | fuck, _ = process.communicate(sf.read()) 189 | 190 | return fuck 191 | 192 | @property 193 | def trace(self): 194 | if self._trace is not None: 195 | return self._trace 196 | 197 | with open(self.filepath, 'rb') as sf: 198 | cmd_args = [ 199 | 'timeout', '2', 200 | shellphish_qemu.qemu_path('cgc-tracer'), 201 | '-d', 'exec', 202 | self.hierarchy._fuzzer.binary_path 203 | ] 204 | #print("cat %s | %s" % (self.filepath, ' '.join(cmd_args))) 205 | process = subprocess.Popen(cmd_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 206 | _, you = process.communicate(sf.read()) 207 | 208 | trace = [ ] 209 | for tline in you.split(b'\n'): 210 | result = re.match(b'Trace 0x[0-9a-fA-F]* \\[([0-9a-fA-F]*)\\]', tline.strip()) 211 | if not result: 212 | continue 213 | trace.append(int(result.group(1), base=16)) 214 | 215 | self._trace = trace 216 | return trace 217 | 218 | @property 219 | def transitions(self): 220 | return [ (self.trace[i], self.trace[i+1]) for i in range(len(self.trace)-1) ] 221 | 222 | @property 223 | def transition_set(self): 224 | return set(self.transitions) 225 | 226 | @property 227 | def new_transitions(self): 228 | if self.sources: 229 | return self.transition_set - set.union(*(s.transition_set for s in self.sources)) 230 | else: 231 | return self.transition_set 232 | 233 | @property 234 | def block_set(self): 235 | return set(self.trace) 236 | 237 | @property 238 | def new_blocks(self): 239 | if self.sources: 240 | return self.block_set - set.union(*(s.block_set for s in self.sources)) 241 | else: 242 | return self.block_set 243 | 244 | @property 245 | def technique_contributions(self): 246 | if self._technique_contributions is not None: 247 | return self._technique_contributions 248 | 249 | results = { 250 | self.contributing_techniques: self.new_transitions 251 | } 252 | if self.sources: 253 | for s in self.sources: 254 | for k,v in s.technique_contributions.items(): 255 | results.setdefault(k,set()).update(v) 256 | self._technique_contributions = results 257 | return results 258 | 259 | @property 260 | def contribution_counts(self): 261 | return { 262 | k: len(v) for k,v in self.technique_contributions.iteritems() 263 | } 264 | 265 | class InputHierarchy(object): 266 | """ 267 | This class deals with the AFL input hierarchy and analyses done on it. 268 | """ 269 | 270 | def __init__(self, fuzzer=None, fuzzer_dir=None, load_crashes=False): 271 | self._fuzzer = fuzzer 272 | self._dir = fuzzer_dir if fuzzer_dir is not None else fuzzer.job_dir 273 | self.inputs = { } 274 | self.instance_inputs = { } 275 | self.instances = [ ] 276 | 277 | self.reload(load_crashes) 278 | 279 | while self._remove_cycles(): 280 | pass 281 | 282 | def _remove_cycles(self): 283 | """ 284 | Really hacky way to remove cycles in hierarchies (wtf). 285 | """ 286 | 287 | G = self.make_graph() 288 | cycles = list(networkx.simple_cycles(G)) 289 | if not cycles: 290 | return False 291 | else: 292 | cycles[0][0].looped = True 293 | cycles[0][0].sources[:] = [ ] 294 | return True 295 | 296 | def triggered_blocks(self): 297 | """ 298 | Gets the triggered blocks by all the testcases. 299 | """ 300 | return set.union(*(i.block_set for i in tqdm.tqdm(self.inputs.values()))) 301 | 302 | def crashes(self): 303 | """ 304 | Returns the crashes, if they are loaded. 305 | """ 306 | return [ i for i in self.inputs.values() if i.crash ] 307 | 308 | def technique_contributions(self): 309 | """ 310 | Get coverage and crashes by technique. 311 | """ 312 | results = { } 313 | for s,(b,c) in self.seed_contributions(): 314 | results.setdefault(s.instance.split('-')[0], [0,0])[0] += b 315 | results.setdefault(s.instance.split('-')[0], [0,0])[1] += c 316 | return results 317 | 318 | def seed_contributions(self): 319 | """ 320 | Get the seeds (including inputs introduced by extensions) that 321 | resulted in coverage and crashes. 322 | """ 323 | sorted_inputs = sorted(( 324 | i for i in self.inputs.values() if i.instance.startswith('fuzzer-') 325 | ), key=lambda j: j.timestamp) 326 | 327 | found = set() 328 | contributions = { } 329 | for s in tqdm.tqdm(sorted_inputs): 330 | o = max(s.origins, key=lambda i: i.timestamp) 331 | if s.crash: 332 | contributions.setdefault(o, (set(),set()))[1].add(s) 333 | else: 334 | c = o.transition_set - found 335 | if not c: 336 | continue 337 | contributions.setdefault(o, (set(),set()))[0].update(c) 338 | found |= c 339 | 340 | return sorted(((k, list(map(len,v))) for k,v in contributions.iteritems()), key=lambda x: x[0].timestamp) 341 | 342 | def reload(self, load_crashes): 343 | self._load_instances() 344 | for i in self.instances: 345 | self._load_inputs(i) 346 | if load_crashes: 347 | self._load_inputs(i, input_type="crashes") 348 | self._resolve_sources() 349 | return self 350 | 351 | def _load_instances(self): 352 | self.instances = [ 353 | os.path.basename(os.path.dirname(n)) 354 | for n in glob.glob(os.path.join(self._dir, "*", "queue")) 355 | ] 356 | self.instance_inputs = { i: { } for i in self.instances } 357 | l.debug("Instances: %s", self.instances) 358 | 359 | def _load_inputs(self, instance, input_type="queue"): 360 | l.info("Loading inputs from instance %s", instance) 361 | for fp in glob.glob(os.path.join(self._dir, instance, input_type, "id*")): 362 | f = os.path.basename(fp) 363 | l.debug("Adding input %s (type %s)", f, input_type) 364 | i = Input(f, instance, self) 365 | self.inputs[i.instance + ':' + i.id] = i 366 | self.instance_inputs[i.instance][i.id] = i 367 | 368 | def _resolve_sources(self): 369 | for i in self.inputs.values(): 370 | i._resolve_sources() 371 | 372 | def instance_input(self, instance, id): #pylint:disable=redefined-builtin 373 | return self.instance_inputs[instance][id] 374 | 375 | def make_graph(self): 376 | G = networkx.DiGraph() 377 | for child in self.inputs.values(): 378 | for parent in child.sources: 379 | G.add_edge(parent, child) 380 | return G 381 | 382 | def plot(self, output=None): 383 | import matplotlib.pyplot as plt #pylint:disable=import-error 384 | plt.close() 385 | networkx.draw(self.make_graph()) 386 | if output: 387 | plt.savefig(output) 388 | else: 389 | plt.show() 390 | -------------------------------------------------------------------------------- /fuzzer/minimizer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import angr 3 | import shutil 4 | import tempfile 5 | import subprocess 6 | import shellphish_afl 7 | from .fuzzer import Fuzzer 8 | 9 | import logging 10 | l = logging.getLogger("fuzzer.Minimizer") 11 | 12 | class Minimizer(object): 13 | """Testcase minimizer""" 14 | 15 | def __init__(self, binary_path, testcase): 16 | """ 17 | :param binary_path: path to the binary which the testcase applies to 18 | :param testcase: string representing the contents of the testcase 19 | """ 20 | 21 | self.binary_path = binary_path 22 | self.testcase = testcase 23 | 24 | Fuzzer._perform_env_checks() 25 | 26 | self.base = Fuzzer._get_base() 27 | l.debug("got base dir %s", self.base) 28 | 29 | # unfortunately here is some code reuse between Fuzzer and Minimizer 30 | p = angr.Project(self.binary_path) 31 | tracer_id = 'cgc' if p.loader.main_object.os == 'cgc' else p.arch.qemu_name 32 | self.tmin_path = os.path.join(shellphish_afl.afl_dir(tracer_id), "afl-tmin") 33 | self.afl_path_var = shellphish_afl.afl_path_var(tracer_id) 34 | 35 | l.debug("tmin_path: %s", self.tmin_path) 36 | l.debug("afl_path_var: %s", self.afl_path_var) 37 | 38 | os.environ['AFL_PATH'] = self.afl_path_var 39 | 40 | # create temp 41 | self.work_dir = tempfile.mkdtemp(prefix='tmin-', dir='/tmp/') 42 | 43 | # flag for work directory removal 44 | self._removed = False 45 | 46 | self.input_testcase = os.path.join(self.work_dir, 'testcase') 47 | self.output_testcase = os.path.join(self.work_dir, 'minimized_result') 48 | 49 | l.debug("input_testcase: %s", self.input_testcase) 50 | l.debug("output_testcase: %s", self.output_testcase) 51 | 52 | # populate contents of input testcase 53 | with open(self.input_testcase, 'wb') as f: 54 | f.write(testcase) 55 | 56 | def __del__(self): 57 | if not self._removed: 58 | shutil.rmtree(self.work_dir) 59 | 60 | def minimize(self): 61 | """Start minimizing""" 62 | 63 | self._start_minimizer().wait() 64 | 65 | with open(self.output_testcase, 'rb') as f: result = f.read() 66 | 67 | shutil.rmtree(self.work_dir) 68 | self._removed = True 69 | 70 | return result 71 | 72 | def _start_minimizer(self, memory="8G"): 73 | 74 | args = [self.tmin_path] 75 | 76 | args += ["-i", self.input_testcase] 77 | args += ["-o", self.output_testcase] 78 | args += ["-m", memory] 79 | args += ["-Q"] 80 | 81 | args += ["--"] 82 | args += [self.binary_path] 83 | 84 | outfile = "minimizer.log" 85 | 86 | l.debug("execing: %s > %s", " ".join(args), outfile) 87 | 88 | outfile = os.path.join(self.work_dir, outfile) 89 | with open(outfile, "wb") as fp: 90 | return subprocess.Popen(args, stderr=fp) 91 | -------------------------------------------------------------------------------- /fuzzer/showmap.py: -------------------------------------------------------------------------------- 1 | import os 2 | import angr 3 | import shutil 4 | import tempfile 5 | import subprocess 6 | import shellphish_afl 7 | from .fuzzer import Fuzzer 8 | 9 | import logging 10 | l = logging.getLogger("fuzzer.Showmap") 11 | 12 | class Showmap(object): 13 | """Show map""" 14 | 15 | def __init__(self, binary_path, testcase, timeout=None): 16 | """ 17 | :param binary_path: path to the binary which the testcase applies to 18 | :param testcase: string representing the contents of the testcase 19 | :param timeout: millisecond timeout 20 | """ 21 | 22 | self.binary_path = binary_path 23 | self.testcase = testcase 24 | self.timeout = None 25 | 26 | if isinstance(binary_path, str): 27 | self.is_multicb = False 28 | self.binaries = [binary_path] 29 | elif isinstance(binary_path, (list,tuple)): 30 | self.is_multicb = True 31 | self.binaries = binary_path 32 | else: 33 | raise ValueError("Was expecting either a string or a list/tuple for binary_path! " 34 | "It's {} instead.".format(type(binary_path))) 35 | 36 | if timeout is not None: 37 | if isinstance(timeout, (int, long)): 38 | self.timeout = str(timeout) 39 | elif isinstance(timeout, (str)): 40 | self.timeout = timeout 41 | else: 42 | raise ValueError("timeout param must be of type int or str") 43 | 44 | # will be set by showmap's return code 45 | self.causes_crash = False 46 | 47 | Fuzzer._perform_env_checks() 48 | 49 | self.base = Fuzzer._get_base() 50 | l.debug("got base dir %s", self.base) 51 | 52 | # unfortunately here is some code reuse between Fuzzer and Minimizer (and Showmap!) 53 | p = angr.Project(self.binaries[0]) 54 | tracer_id = 'cgc' if p.loader.main_object.os == 'cgc' else p.arch.qemu_name 55 | if self.is_multicb: 56 | tracer_id = 'multi-{}'.format(tracer_id) 57 | 58 | self.showmap_path = os.path.join(shellphish_afl.afl_dir(tracer_id), "afl-showmap") 59 | self.afl_path_var = shellphish_afl.afl_path_var(tracer_id) 60 | 61 | l.debug("showmap_path: %s", self.showmap_path) 62 | l.debug("afl_path_var: %s", self.afl_path_var) 63 | 64 | os.environ['AFL_PATH'] = self.afl_path_var 65 | 66 | # create temp 67 | self.work_dir = tempfile.mkdtemp(prefix='showmap-', dir='/tmp/') 68 | 69 | # flag for work directory removal 70 | self._removed = False 71 | 72 | self.input_testcase = os.path.join(self.work_dir, 'testcase') 73 | self.output = os.path.join(self.work_dir, 'out') 74 | 75 | l.debug("input_testcase: %s", self.input_testcase) 76 | l.debug("output: %s", self.output) 77 | 78 | # populate contents of input testcase 79 | with open(self.input_testcase, 'wb') as f: 80 | f.write(testcase) 81 | 82 | def __del__(self): 83 | if not self._removed: 84 | shutil.rmtree(self.work_dir) 85 | 86 | def showmap(self): 87 | """Create the map""" 88 | 89 | if self._start_showmap().wait() == 2: 90 | self.causes_crash = True 91 | 92 | with open(self.output) as f: result = f.read() 93 | 94 | shutil.rmtree(self.work_dir) 95 | self._removed = True 96 | 97 | shownmap = dict() 98 | for line in result.split("\n")[:-1]: 99 | key, h_count = map(int, line.split(":")) 100 | shownmap[key] = h_count 101 | 102 | return shownmap 103 | 104 | def _start_showmap(self, memory="8G"): 105 | 106 | args = [self.showmap_path] 107 | 108 | args += ["-o", self.output] 109 | if not self.is_multicb: 110 | args += ["-m", memory] 111 | args += ["-Q"] 112 | 113 | if self.timeout: 114 | args += ["-t", self.timeout] 115 | else: 116 | args += ["-t", "%d+" % (len(self.binaries) * 1000)] 117 | 118 | args += ["--"] 119 | args += self.binaries 120 | 121 | outfile = "minimizer.log" 122 | 123 | l.debug("execing: %s > %s", " ".join(args), outfile) 124 | 125 | outfile = os.path.join(self.work_dir, outfile) 126 | with open(outfile, "w") as fp, open(self.input_testcase, 'rb') as it, open("/dev/null", 'wb') as devnull: 127 | return subprocess.Popen(args, stdin=it, stdout=devnull, stderr=fp, close_fds=True) 128 | -------------------------------------------------------------------------------- /reqs.txt: -------------------------------------------------------------------------------- 1 | angr 2 | shellphish-qemu 3 | shellphish-afl 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.core import setup 3 | 4 | setup( 5 | name='fuzzer', version='1.1', description="Python wrapper for multiarch AFL", 6 | packages=['fuzzer', 'fuzzer.extensions'], 7 | data_files = [ ("bin", (os.path.join("bin", "create_dict.py"),)) ], 8 | scripts = [ 'shellphuzz' ], 9 | install_requires=['angr', 'shellphish-qemu', 'shellphish-afl', 'tqdm'] 10 | ) 11 | -------------------------------------------------------------------------------- /shellphuzz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import imp 6 | import time 7 | import fuzzer 8 | import shutil 9 | import socket 10 | import driller 11 | import tarfile 12 | import argparse 13 | import importlib 14 | import logging.config 15 | 16 | if __name__ == "__main__": 17 | parser = argparse.ArgumentParser(description="Shellphish fuzzer interface") 18 | parser.add_argument('binary', help="the path to the target binary to fuzz") 19 | parser.add_argument('-g', '--grease-with', help="A directory of inputs to grease the fuzzer with when it gets stuck.") 20 | parser.add_argument('-d', '--driller_workers', help="When the fuzzer gets stuck, drill with N workers.", type=int) 21 | parser.add_argument('-f', '--force_interval', help="Force greaser/fuzzer assistance at a regular interval (in seconds).", type=float) 22 | parser.add_argument('-w', '--work-dir', help="The work directory for AFL.", default="/dev/shm/work/") 23 | parser.add_argument('-c', '--afl-cores', help="Number of AFL workers to spin up.", default=1, type=int) 24 | parser.add_argument('-C', '--first-crash', help="Stop on the first crash.", action='store_true', default=False) 25 | parser.add_argument('-t', '--timeout', help="Timeout (in seconds).", type=float) 26 | parser.add_argument('-i', '--ipython', help="Drop into ipython after starting the fuzzer.", action='store_true') 27 | parser.add_argument('-T', '--tarball', help="Tarball the resulting AFL workdir for further analysis to this file -- '{}' is replaced with the hostname.") 28 | parser.add_argument('-m', '--helper-module', help="A module that includes some helper scripts for seed selection and such.") 29 | parser.add_argument('--memory', help="Memory limit to pass to AFL (MB, or use k, M, G, T suffixes)", default="8G") 30 | parser.add_argument('--no-dictionary', help="Do not create a dictionary before fuzzing.", action='store_true', default=False) 31 | parser.add_argument('--logcfg', help="The logging configuration file.", default=".shellphuzz.ini") 32 | parser.add_argument('-s', '--seed-dir', action="append", help="Directory of files to seed fuzzer with") 33 | parser.add_argument('--run-timeout', help="Number of seconds permitted for each run of binary", type=int) 34 | parser.add_argument('--driller-timeout', help="Number of seconds to allow driller to run", type=int, default=10*60) 35 | parser.add_argument('--length-extension', help="Try extending inputs to driller by this many bytes", type=int) 36 | args = parser.parse_args() 37 | 38 | if os.path.isfile(os.path.join(os.getcwd(), args.logcfg)): 39 | logging.config.fileConfig(os.path.join(os.getcwd(), args.logcfg)) 40 | 41 | try: os.mkdir("/dev/shm/work/") 42 | except OSError: pass 43 | 44 | if args.helper_module: 45 | try: 46 | helper_module = importlib.import_module(args.helper_module) 47 | except (ImportError, TypeError): 48 | helper_module = imp.load_source('fuzzing_helper', args.helper_module) 49 | else: 50 | helper_module = None 51 | 52 | drill_extension = None 53 | grease_extension = None 54 | 55 | if args.grease_with: 56 | print ("[*] Greasing...") 57 | grease_extension = fuzzer.GreaseCallback( 58 | args.grease_with, 59 | grease_filter=helper_module.grease_filter if helper_module is not None else None, 60 | grease_sorter=helper_module.grease_sorter if helper_module is not None else None 61 | ) 62 | if args.driller_workers: 63 | print ("[*] Drilling...") 64 | drill_extension = driller.LocalCallback(num_workers=args.driller_workers, worker_timeout=args.driller_timeout, length_extension=args.length_extension) 65 | 66 | stuck_callback = ( 67 | (lambda f: (grease_extension(f), drill_extension(f))) if drill_extension and grease_extension 68 | else drill_extension or grease_extension 69 | ) 70 | 71 | seeds = None 72 | if args.seed_dir: 73 | seeds = [] 74 | print ("[*] Seeding...") 75 | for dirpath in args.seed_dir: 76 | for filename in os.listdir(dirpath): 77 | filepath = os.path.join(dirpath, filename) 78 | if not os.path.isfile(filepath): 79 | continue 80 | with open(filepath, 'rb') as seedfile: 81 | seeds.append(seedfile.read()) 82 | 83 | print ("[*] Creating fuzzer...") 84 | fuzzer = fuzzer.Fuzzer( 85 | args.binary, args.work_dir, afl_count=args.afl_cores, force_interval=args.force_interval, 86 | create_dictionary=not args.no_dictionary, stuck_callback=stuck_callback, time_limit=args.timeout, 87 | memory=args.memory, seeds=seeds, timeout=args.run_timeout, 88 | ) 89 | 90 | # start it! 91 | print ("[*] Starting fuzzer...") 92 | fuzzer.start() 93 | 94 | if args.ipython: 95 | print ("[!]") 96 | print ("[!] Launching ipython shell. Relevant variables:") 97 | print ("[!]") 98 | print ("[!] fuzzer") 99 | print ("[!] driller_extension") 100 | print ("[!] grease_extension") 101 | print ("[!]") 102 | import IPython; IPython.embed() 103 | 104 | try: 105 | print ("[*] Waiting for fuzzer completion (timeout: %s, first_crash: %s)." % (args.timeout, args.first_crash)) 106 | 107 | crash_seen = False 108 | while True: 109 | time.sleep(5) 110 | if not crash_seen and fuzzer.found_crash(): 111 | print ("[*] Crash found!") 112 | crash_seen = True 113 | if args.first_crash: 114 | break 115 | if fuzzer.timed_out(): 116 | print ("[*] Timeout reached.") 117 | break 118 | except KeyboardInterrupt: 119 | print ("[*] Aborting wait. Ctrl-C again for KeyboardInterrupt.") 120 | except Exception as e: 121 | print ("[*] Unknown exception received (%s). Terminating fuzzer." % e) 122 | fuzzer.kill() 123 | if drill_extension: 124 | drill_extension.kill() 125 | raise 126 | 127 | print ("[*] Terminating fuzzer.") 128 | fuzzer.kill() 129 | if drill_extension: 130 | drill_extension.kill() 131 | 132 | if args.tarball: 133 | print ("[*] Dumping results...") 134 | p = os.path.join("/tmp/", "afl_sync") 135 | try: 136 | shutil.rmtree(p) 137 | except (OSError, IOError): 138 | pass 139 | shutil.copytree(fuzzer.out_dir, p) 140 | 141 | tar_name = args.tarball.replace("{}", socket.gethostname()) 142 | 143 | tar = tarfile.open("/tmp/afl_sync.tar.gz", "w:gz") 144 | tar.add(p, arcname=socket.gethostname()+'-'+os.path.basename(args.binary)) 145 | tar.close() 146 | print ("[*] Copying out result tarball to %s" % tar_name) 147 | shutil.move("/tmp/afl_sync.tar.gz", tar_name) 148 | -------------------------------------------------------------------------------- /tests/test_fuzzer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import nose 3 | import tempfile 4 | import subprocess 5 | import fuzzer 6 | 7 | import logging 8 | l = logging.getLogger("fuzzer.tests.test_fuzzer") 9 | 10 | import os 11 | bin_location = str(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../binaries')) 12 | fuzzer_bin = str(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../bin')) 13 | 14 | def test_dictionary_creation_cgc(): 15 | ''' 16 | test dictionary creation on a binary 17 | ''' 18 | 19 | binary = os.path.join(bin_location, "tests/cgc/ccf3d301_01") 20 | out_dict = tempfile.mktemp(prefix='fuzztest', dir='/tmp') 21 | 22 | args = [os.path.join(fuzzer_bin, 'create_dict.py'), binary] 23 | 24 | with open(out_dict, "wb") as f: 25 | p = subprocess.Popen(args, stdout=f) 26 | 27 | retcode = p.wait() 28 | 29 | nose.tools.assert_equal(retcode, 0) 30 | 31 | dict_data = open(out_dict).read() 32 | os.remove(out_dict) 33 | 34 | definitions = dict_data.split("\n") 35 | 36 | # assert we find just as definitions 37 | nose.tools.assert_true(len(definitions) >= 60) 38 | 39 | def test_minimizer(): 40 | """ 41 | Test minimization of an input 42 | """ 43 | 44 | binary = os.path.join(bin_location, "tests/cgc/PIZZA_00001") 45 | 46 | crash = bytes.fromhex('66757fbeff10ff7f1c3131313131413131317110314301000080006980009fdce6fecc4c66747fbeffffff7f1c31313131314131313171793143cfcfcfcfcfcfcf017110314301000000003e3e3e3e3e413e3e2e3e3e383e317110000000003e3e3e3e3e413e3e2e3e3e383e31713631310031103c3b6900ff3e3131413131317110313100000000006900ff91dce6fecc7e6e000200fecc4c66747fbeffffff7f1c31313131314131313171793143cf003100000000006900ff91dcc3c3c3479fdcffff084c3131313133313141314c6f00003e3e3e3e30413e3e2e3e3e383e31712a000000003e3e3e3e3eedededededededededededededededededededededededededededededededededededededededededede0dadada4c4c4c4c333054c4c4c401000000fb6880009fdce6fecc4c66757fbeffffff7f1c31313131314131313171793143cfcfcfcfcfcfcf017110314301000000003e3e3e3e3e413e3e2e343e383e317110000000003e3e3e3e3e413e3e2e3e3e383e31713631310031103c3b6900ff3e3131413131317110313100000000006900ff91dce6fecc7e6e000200003100000000006900ff91dcc3c3c3479fdcffff084c0d0d0d0d0dfa1d7f') 47 | 48 | m = fuzzer.Minimizer(binary, crash) 49 | 50 | nose.tools.assert_equal(m.minimize(), b'100') 51 | 52 | def test_showmap(): 53 | """ 54 | Test the mapping of an input 55 | """ 56 | 57 | true_dict = {7525: 1, 14981: 1, 25424: 1, 31473: 1, 33214: 1, 37711: 1, 64937: 1, 65353: 4, 66166: 1, 79477: 1, 86259: 1, 86387: 1, 96625: 1, 107932: 1, 116010: 1, 116490: 1, 117482: 4, 120443: 1} 58 | 59 | binary = os.path.join(bin_location, "tests/cgc/cfe_CADET_00003") 60 | 61 | testcase = b"hello" 62 | 63 | s = fuzzer.Showmap(binary, testcase) 64 | smap = s.showmap() 65 | 66 | for te in true_dict.keys(): 67 | nose.tools.assert_equal(true_dict[te], smap[te]) 68 | 69 | def test_fuzzer_spawn(): 70 | """ 71 | Test that the fuzzer spawns correctly 72 | """ 73 | 74 | binary = os.path.join(bin_location, "tests/cgc/PIZZA_00001") 75 | 76 | f = fuzzer.Fuzzer(binary, "work") 77 | f.start() 78 | 79 | for _ in range(15): 80 | if f.alive: 81 | break 82 | time.sleep(1) 83 | 84 | nose.tools.assert_true(f.alive) 85 | if f.alive: 86 | f.kill() 87 | 88 | def test_multicb_spawn(): 89 | """ 90 | Test that the fuzzer spins up for a multicb challenge. 91 | """ 92 | 93 | binaries = [os.path.join(bin_location, "tests/cgc/251abc02_01"), 94 | os.path.join(bin_location, "tests/cgc/251abc02_02")] 95 | 96 | f = fuzzer.Fuzzer(binaries, "work", create_dictionary=True) 97 | f.start() 98 | 99 | for _ in range(15): 100 | if f.alive: 101 | break 102 | time.sleep(1) 103 | 104 | nose.tools.assert_true(f.alive) 105 | 106 | dictionary_path = os.path.join("work", "251abc02_01", "251abc02_01.dict") 107 | 108 | nose.tools.assert_true(os.path.isfile(dictionary_path)) 109 | 110 | if f.alive: 111 | f.kill() 112 | 113 | def run_all(): 114 | functions = globals() 115 | all_functions = dict(filter((lambda kv: kv[0].startswith('test_')), functions.items())) 116 | for f in sorted(all_functions.keys()): 117 | if hasattr(all_functions[f], '__call__'): 118 | all_functions[f]() 119 | 120 | if __name__ == "__main__": 121 | logging.getLogger("fuzzer").setLevel("DEBUG") 122 | run_all() 123 | --------------------------------------------------------------------------------