├── LICENSE ├── README.md ├── common ├── __init__.py ├── color.py ├── config.py ├── debug.py ├── execution_result.py ├── qemu.py ├── qemu_protocol.py ├── safe_syscall.py ├── self_check.py └── util.py ├── debug ├── __init__.py └── core.py ├── fuzzer ├── __init__.py ├── bitmap.py ├── communicator.py ├── core.py ├── native │ └── bitmap.c ├── node.py ├── process │ ├── __init__.py │ ├── master.py │ └── slave.py ├── queue.py ├── scheduler.py ├── state_logic.py ├── statistics.py └── technique │ ├── __init__.py │ ├── arithmetic.py │ ├── bitflip.py │ ├── debug.py │ ├── grimoire_inference.py │ ├── grimoire_mutations.py │ ├── havoc.py │ ├── havoc_handler.py │ ├── helper.py │ ├── interesting_values.py │ ├── radamsa │ ├── radamsa.py │ ├── redqueen │ ├── __init__.py │ ├── cmp.py │ ├── colorize.py │ ├── encoding.py │ ├── hash_fix.py │ ├── hash_patch.py │ ├── mod.py │ ├── parser.py │ └── workdir.py │ └── trim.py ├── help.txt ├── info ├── __init__.py └── core.py ├── kafl_debug.py ├── kafl_fuzz.py ├── kafl_info.py ├── kafl_user_prepare.py ├── mcat.py ├── paper.png └── qemu.patch /README.md: -------------------------------------------------------------------------------- 1 | # Grimoire: Synthesizing Structure while Fuzzing 2 | 3 | 4 | Grimoire is coverage-guided fuzzer for structured input languages. It is built upon [Redqueen](https://github.com/RUB-SysSec/redqueen). 5 | 6 | The fuzzer is based on our [paper](https://www.usenix.org/system/files/sec19-blazytko.pdf) ([slides](https://www.usenix.org/sites/default/files/conference/protected-files/sec19_slides_blazytko.pdf), [recording](https://www.youtube.com/watch?v=VF9YcAnzMPI)): 7 | 8 | ``` 9 | @inproceedings{blazytko2019grimoire, 10 | author = {Tim Blazytko and Cornelius Aschermann and Moritz Schl{\"o}gel and Ali Abbasi and Sergej Schumilo and Simon W{\"o}rner and Thorsten Holz}, 11 | title = {{GRIMOIRE}: Synthesizing Structure while Fuzzing},, 12 | year = {2019}, 13 | booktitle = {USENIX Security Symposium} 14 | } 15 | ``` 16 | 17 | # Setup 18 | 19 | 1. Setup [Redqueen](https://github.com/RUB-SysSec/redqueen) 20 | 2. Apply Patch `qemu.patch` to `QEMU-PT` 21 | 3. Use this python code base instead of Redqueen's `kAFL-Fuzzer` 22 | 4. Create a string dictionary (for string mutations) via `strings -n3 -d | grep -v "\s" | sort | uniq > dict.txt` 23 | 5. Prepare binary and fuzz as described [here](https://github.com/RUB-SysSec/redqueen#initial-setup). To use the dictionary, add `-I ` to `kafl_fuzz.py`. 24 | 25 | 26 | 27 | # Code 28 | 29 | Grimoire can be understood as a patch applied to Redqueen's code base. The published source code contains Redqueen's fuzzing logic, the implementation of 30 | Grimoire as well as its interaction with Redqueen. 31 | 32 | In detail, Grimoire's is organized as follows: 33 | 34 | ## Inference Logic 35 | 36 | The inference logic (paper section 3.1) is contained in `fuzzer/technique/grimoire_inference.py`. 37 | 38 | ## Mutations 39 | 40 | Grimoire's large-scale mutations (paper section 3.2) are contained in `fuzzer/technique/grimoire_mutations.py`. 41 | 42 | ## Interaction with Redqueen 43 | 44 | `fuzzer/scheduler.py` defines `GrimoireScheduler`, which is used in Redqueen's `InputQueue` in file `fuzzer/queue.py`. Everytime a `SlaveProcess` (`fuzzer/process/slave.py`) requests a new input from the queue, a non-generalized input that triggered new coverage will be returned. 45 | 46 | This input is then generalized in the state `grimoire_inference` in `FuzzingStateLogic` (`fuzzer/state_logic.py`) 47 | 48 | 49 | In a later fuzzing stage---during Redqueen's havoc mutation phase---Grimoire's mutations will be applied in `FuzzingStateLogic`. 50 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RUB-SysSec/grimoire/fae90ee436fe626f54ec2421269de8340cf06bf9/common/__init__.py -------------------------------------------------------------------------------- /common/color.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | HEADER = '\033[95m' 19 | OKBLUE = '\033[94m' 20 | OKGREEN = '\033[92m' 21 | WARNING = '\033[0;33m' 22 | FAIL = '\033[91m' 23 | ENDC = '\033[0m' 24 | CLRSCR = '\x1b[1;1H' 25 | REALCLRSCR = '\x1b[2J' 26 | BOLD = '\033[1m' 27 | FLUSH_LINE = '\r\x1b[K' 28 | 29 | 30 | def MOVE_CURSOR_UP(num): 31 | return "\033[" + str(num) + "A" 32 | 33 | 34 | def MOVE_CURSOR_DOWN(num): 35 | return "\033[" + str(num) + "B" 36 | 37 | 38 | def MOVE_CURSOR_LEFT(num): 39 | return "\033[" + str(num) + "C" 40 | 41 | 42 | def MOVE_CURSOR_RIGHT(num): 43 | return "\033[" + str(num) + "D" 44 | 45 | 46 | HLINE = unichr(0x2500) 47 | VLINE = unichr(0x2502) 48 | VLLINE = unichr(0x2524) 49 | VRLINE = unichr(0x251c) 50 | LBEDGE = unichr(0x2514) 51 | RBEDGE = unichr(0x2518) 52 | HULINE = unichr(0x2534) 53 | HDLINE = unichr(0x252c) 54 | LTEDGE = unichr(0x250c) 55 | RTEDGE = unichr(0x2510) 56 | 57 | INFO_PREFIX = "[INFO] " 58 | ERROR_PREFIX = "[ERROR] " 59 | WARNING_PREFIX = "[WARNING] " 60 | -------------------------------------------------------------------------------- /common/debug.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import collections 19 | import sys 20 | import time 21 | from datetime import timedelta 22 | from multiprocessing import Manager 23 | 24 | __author__ = 'sergej' 25 | 26 | 27 | def hexdump(src, length=16): 28 | hexdump_filter = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) 29 | lines = [] 30 | for c in xrange(0, len(src), length): 31 | chars = src[c:c + length] 32 | hex_value = ' '.join(["%02x" % ord(x) for x in chars]) 33 | printable = ''.join(["%s" % ((ord(x) <= 127 and hexdump_filter[ord(x)]) or '.') for x in chars]) 34 | lines.append("%04x %-*s %s\n" % (c, length * 3, hex_value, printable)) 35 | return ''.join(lines) 36 | 37 | 38 | logging_is_enabled = False 39 | debug_file_path = None 40 | output_file = None 41 | init_time = 0.0 42 | 43 | log_prefix = "" 44 | 45 | manager = Manager() 46 | shared_list = manager.list() 47 | 48 | 49 | def configure_log_prefix(str): 50 | global log_prefix 51 | log_prefix = str + " " 52 | 53 | 54 | def __init_logger(): 55 | global output_file, init_time, debug_file_path 56 | init_time = time.time() 57 | output_file = open(debug_file_path, 'w') 58 | 59 | 60 | def logger(msg): 61 | global logging_is_enabled, output_file, init_time, shared_list, log_prefix 62 | 63 | try: 64 | if (len(shared_list) >= 9): 65 | shared_list.pop(0) 66 | shared_list.append(msg.replace("\n", " ")) 67 | except: 68 | pass 69 | if logging_is_enabled: 70 | if not output_file: 71 | __init_logger() 72 | output_file.write("[" + str(timedelta(seconds=time.time() - init_time)) + "] " + log_prefix + msg + "\n") 73 | output_file.flush() 74 | 75 | 76 | def get_rbuf_content(): 77 | global shared_list 78 | try: 79 | return list(shared_list) 80 | except: 81 | return None 82 | 83 | 84 | def enable_logging(workdir): 85 | global logging_is_enabled, debug_file_path 86 | logging_is_enabled = True 87 | debug_file_path = workdir + "/debug.log" 88 | 89 | 90 | def log_master(msg): 91 | logger("[MASTER] " + msg) 92 | 93 | 94 | def log_mapserver(msg): 95 | logger("[MAPSERV] " + msg) 96 | 97 | 98 | def log_update(msg): 99 | logger("[UPDATE] " + msg) 100 | 101 | 102 | def log_slave(msg, qid): 103 | if qid < 10: 104 | logger("[SLAVE " + str(qid) + "] " + msg) 105 | elif qid > 10 and qid < 100: 106 | logger("[SLAVE " + str(qid) + "] " + msg) 107 | else: 108 | logger("[SLAVE " + str(qid) + "] " + msg) 109 | 110 | 111 | def log_tree(msg): 112 | logger("[TREE] " + msg) 113 | 114 | 115 | def log_eval(msg): 116 | logger("[EVAL] " + msg) 117 | 118 | 119 | def log_redq(msg): 120 | logger("[RQ] " + msg) 121 | 122 | 123 | def log_grimoire(msg): 124 | from common.safe_syscall import safe_print 125 | safe_print(msg) 126 | logger("[GI] " + msg) 127 | 128 | 129 | def log_qemu(msg, qid): 130 | if qid < 10: 131 | logger("[QEMU " + str(qid) + "] " + msg) 132 | elif qid > 10 and qid < 100: 133 | logger("[QEMU " + str(qid) + "] " + msg) 134 | else: 135 | logger("[QEMU " + str(qid) + "] " + msg) 136 | 137 | 138 | def log_core(msg): 139 | logger("[CORE] " + msg) 140 | 141 | 142 | def log_info(msg): 143 | logger("[INFO] " + msg) 144 | -------------------------------------------------------------------------------- /common/execution_result.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import ctypes 18 | import mmh3 19 | 20 | from fuzzer.bitmap import GlobalBitmap 21 | 22 | 23 | class ExecutionResult: 24 | 25 | @staticmethod 26 | def bitmap_from_bytearray(bitmap, exitreason, performance): 27 | bitmap_size = len(bitmap) 28 | c_bitmap = (ctypes.c_uint8 * bitmap_size).from_buffer_copy(bitmap) 29 | return ExecutionResult(c_bitmap, bitmap_size, exitreason, performance) 30 | 31 | def __init__(self, cbuffer, bitmap_size, exit_reason, performance): 32 | self.bitmap_size = bitmap_size 33 | self.cbuffer = cbuffer 34 | self.lut_applied = False # By default we assume that the bucket lut has not yet been applied 35 | self.exit_reason = exit_reason 36 | self.performance = performance 37 | 38 | def invalidate(self): 39 | self.cbuffer = None 40 | return self 41 | 42 | def is_lut_applied(self): 43 | return self.lut_applied 44 | 45 | def copy_to_array(self): 46 | return bytearray(self.cbuffer) 47 | 48 | def hash(self): 49 | return mmh3.hash(self.cbuffer) 50 | 51 | def apply_lut(self): 52 | assert not self.lut_applied 53 | GlobalBitmap.apply_lut(self) 54 | assert self.lut_applied 55 | return self 56 | -------------------------------------------------------------------------------- /common/qemu_protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | ACQUIRE = 'R' 18 | RELEASE = 'D' 19 | 20 | RELOAD = 'L' 21 | ENABLE_SAMPLING = 'S' 22 | DISABLE_SAMPLING = 'O' 23 | COMMIT_FILTER = 'T' 24 | FINALIZE = 'F' 25 | 26 | ENABLE_RQI_MODE = 'A' 27 | DISABLE_RQI_MODE = 'B' 28 | ENABLE_TRACE_MODE = 'E' 29 | DISABLE_TRACE_MODE = 'G' 30 | ENABLE_PATCHES = 'P' 31 | DISABLE_PATCHES = 'Q' 32 | REDQUEEN_SET_LIGHT_INSTRUMENTATION = 'U' 33 | REDQUEEN_SET_SE_INSTRUMENTATION = 'V' 34 | REDQUEEN_SET_WHITELIST_INSTRUMENTATION = 'W' 35 | REDQUEEN_SET_BLACKLIST = 'X' 36 | 37 | CRASH = 'C' 38 | KASAN = 'K' 39 | INFO = 'I' 40 | TIMEOUT = 't' 41 | 42 | PRINTF = 'X' 43 | 44 | PT_TRASHED = 'Z' 45 | PT_TRASHED_CRASH = 'M' 46 | PT_TRASHED_KASAN = 'N' 47 | 48 | ABORT = 'H' 49 | 50 | CMDS = { 51 | ACQUIRE: "ACQUIRE", 52 | RELEASE: "RELEASE", 53 | RELOAD: "RELOAD", 54 | 55 | ENABLE_SAMPLING: "ENABLE_SAMPLING", 56 | DISABLE_SAMPLING: "DISABLE_SAMPLING", 57 | COMMIT_FILTER: "COMMIT_FILTER", 58 | FINALIZE: "FINALIZE", 59 | 60 | ENABLE_RQI_MODE: "ENABLE_RQI_MODE", 61 | DISABLE_RQI_MODE: "DISABLE_RQI_MODE", 62 | 63 | ENABLE_TRACE_MODE: "ENABLE_TRACE_MODE", 64 | DISABLE_TRACE_MODE: "DISABLE_TRACE_MODE", 65 | ENABLE_PATCHES: "ENABLE_PATCHES", 66 | DISABLE_PATCHES: "DISABLE_PATCHES", 67 | REDQUEEN_SET_LIGHT_INSTRUMENTATION: "REDQUEEN_SET_LIGHT_INSTRUMENTATION", 68 | REDQUEEN_SET_SE_INSTRUMENTATION: "REDQUEEN_SET_SE_INSTRUMENTATION", 69 | REDQUEEN_SET_WHITELIST_INSTRUMENTATION: "REDQUEEN_SET_WHITELIST_INSTRUMENTATION", 70 | REDQUEEN_SET_BLACKLIST: "REDQUEEN_SET_BLACKLIST", 71 | 72 | CRASH: "CRASH", 73 | KASAN: "KASAN", 74 | INFO: "INFO", 75 | 76 | PRINTF: "PRINTF", 77 | 78 | PT_TRASHED: "PT_TRASHED", 79 | PT_TRASHED_CRASH: "PT_TRASHED_CRASH", 80 | PT_TRASHED_KASAN: "PT_TRASHED_KASAN", 81 | 82 | ABORT: "ABORT", 83 | } 84 | -------------------------------------------------------------------------------- /common/safe_syscall.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import select 18 | import socket 19 | 20 | from common.debug import logger 21 | 22 | INTR_ERRNO = 4 23 | 24 | 25 | def safe_print(str_input): 26 | try: 27 | print(str_input).encode('utf-8') 28 | except OSError and IOError as e: 29 | pass # IOError: [Errno 11] Resource temporarily unavailable 30 | 31 | 32 | def safe_select(rlist, wlist, xlist, timeout): 33 | global INTR_ERRNO 34 | rvalue = None 35 | while True: 36 | try: 37 | rvalue = select.select(rlist, wlist, xlist, timeout) 38 | break 39 | except select.error as e: 40 | if e.args[0] == INTR_ERRNO: 41 | continue 42 | else: 43 | raise 44 | return rvalue 45 | 46 | 47 | class safe_socket(socket.socket): 48 | def __init__(self, family): 49 | super(safe_socket, self).__init__(family) 50 | 51 | def connect(self, address): 52 | global INTR_ERRNO 53 | while True: 54 | try: 55 | super(safe_socket, self).connect(address) 56 | break 57 | except OSError as e: 58 | if e.args[0] == INTR_ERRNO: 59 | continue 60 | else: 61 | raise 62 | 63 | def settimeout(self, value): 64 | global INTR_ERRNO 65 | while True: 66 | try: 67 | super(safe_socket, self).settimeout(value) 68 | break 69 | except OSError as e: 70 | if e.args[0] == INTR_ERRNO: 71 | continue 72 | else: 73 | raise 74 | 75 | def setblocking(self, flag): 76 | global INTR_ERRNO 77 | while True: 78 | try: 79 | super(safe_socket, self).setblocking(flag) 80 | break 81 | except OSError as e: 82 | if e.args[0] == INTR_ERRNO: 83 | continue 84 | else: 85 | raise 86 | 87 | def send(self, data): 88 | global INTR_ERRNO 89 | rvalue = None 90 | while True: 91 | try: 92 | rvalue = super(safe_socket, self).send(data) 93 | except OSError as e: 94 | if e.args[0] == INTR_ERRNO: 95 | continue 96 | else: 97 | raise 98 | return rvalue 99 | 100 | def recv(self, size): 101 | global INTR_ERRNO 102 | rvalue = None 103 | while True: 104 | try: 105 | rvalue = super(safe_socket, self).recv(size) 106 | except (OSError, IOError) as e: 107 | if e.args[0] == INTR_ERRNO or e.errno == errno.EINTR: 108 | continue 109 | else: 110 | raise 111 | return rvalue 112 | 113 | def close(self): 114 | global INTR_ERRNO 115 | rvalue = None 116 | while True: 117 | try: 118 | rvalue = super(safe_socket, self).close() 119 | except OSError as e: 120 | if e.args[0] == INTR_ERRNO: 121 | continue 122 | else: 123 | raise 124 | return rvalue 125 | -------------------------------------------------------------------------------- /common/self_check.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import os 19 | import subprocess 20 | import sys 21 | from fcntl import ioctl 22 | 23 | import common.color 24 | from common.color import WARNING_PREFIX, ERROR_PREFIX, FAIL, WARNING, ENDC 25 | 26 | 27 | def check_if_nativ_lib_compiled(): 28 | if not (os.path.exists(os.path.dirname(sys.argv[0]) + "/fuzzer/native/") and os.path.exists( 29 | os.path.dirname(sys.argv[0]) + "/fuzzer/native/bitmap.so")): 30 | print(WARNING + WARNING_PREFIX + "bitmap.so file does not exist. Compiling..." + ENDC) 31 | 32 | current_dir = os.getcwd() 33 | os.chdir(os.path.dirname(sys.argv[0])) 34 | p = subprocess.Popen(("gcc fuzzer/native/bitmap.c --shared -fPIC -O3 -o fuzzer/native/bitmap.so").split(" "), 35 | stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) 36 | if p.wait() != 0: 37 | print(FAIL + ERROR_PREFIX + "Compiling failed..." + ENDC) 38 | os.chdir(current_dir) 39 | return False 40 | return True 41 | 42 | 43 | def check_if_installed(cmd): 44 | p = subprocess.Popen(("which " + cmd).split(" "), stdout=subprocess.PIPE, stdin=subprocess.PIPE, 45 | stderr=subprocess.PIPE) 46 | if p.wait() != 0: 47 | return False 48 | return True 49 | 50 | 51 | def check_version(): 52 | if sys.version_info < (2, 7, 0) or sys.version_info >= (3, 0, 0): 53 | print(FAIL + ERROR_PREFIX + "This script requires python 2.7 or higher (except for python 3.x)!" + ENDC) 54 | return False 55 | return True 56 | 57 | 58 | def check_packages(): 59 | try: 60 | import mmh3 61 | except ImportError: 62 | print(FAIL + ERROR_PREFIX + "Package 'mmh3' is missing!" + ENDC) 63 | return False 64 | 65 | try: 66 | import lz4 67 | except ImportError: 68 | print(FAIL + ERROR_PREFIX + "Package 'lz4' is missing!" + ENDC) 69 | return False 70 | 71 | try: 72 | import psutil 73 | except ImportError: 74 | print(FAIL + ERROR_PREFIX + "Package 'psutil' is missing!" + ENDC) 75 | return False 76 | 77 | try: 78 | import pygraphviz 79 | except ImportError: 80 | print(FAIL + ERROR_PREFIX + "Package 'pygraphviz' is missing!" + ENDC) 81 | return False 82 | 83 | if not check_if_installed("lddtree"): 84 | print(FAIL + ERROR_PREFIX + "Tool 'lddtree' is missing (Hint: run `sudo apt install pax-utils`)!" + ENDC) 85 | return False 86 | 87 | try: 88 | import ipdb 89 | except ImportError: 90 | print(FAIL + ERROR_PREFIX + "Package 'ipdb' is missing (Hint: run `sudo pip install ipdb`)!" + ENDC) 91 | return False 92 | 93 | try: 94 | import fastrand 95 | except ImportError: 96 | print( 97 | FAIL + ERROR_PREFIX + "Package 'fastrand' is missing (Hint: run `python setup.py install` in the fastrand folder)!" + ENDC) 98 | return False 99 | 100 | return True 101 | 102 | 103 | def check_vmx_pt(): 104 | from fcntl import ioctl 105 | 106 | KVMIO = 0xAE 107 | KVM_VMX_PT_SUPPORTED = KVMIO << (8) | 0xe4 108 | 109 | try: 110 | fd = open("/dev/kvm", "wb") 111 | except: 112 | print(FAIL + ERROR_PREFIX + "KVM is not loaded!" + ENDC) 113 | return False 114 | 115 | try: 116 | ret = ioctl(fd, KVM_VMX_PT_SUPPORTED, 0) 117 | except IOError: 118 | print(FAIL + ERROR_PREFIX + "VMX_PT is not loaded!" + ENDC) 119 | return False 120 | fd.close() 121 | 122 | if ret == 0: 123 | print(FAIL + ERROR_PREFIX + "Intel PT is not supported on this CPU!" + ENDC) 124 | return False 125 | 126 | return True 127 | 128 | 129 | def check_apple_osk(config): 130 | if config.argument_values["macOS"]: 131 | if config.config_values["APPLE-SMC-OSK"] == "": 132 | print(FAIL + ERROR_PREFIX + "APPLE SMC OSK is missing in kafl.ini!" + ENDC) 133 | return False 134 | return True 135 | 136 | 137 | def check_apple_ignore_msrs(config): 138 | if config.argument_values["macOS"]: 139 | try: 140 | f = open("/sys/module/kvm/parameters/ignore_msrs") 141 | if not 'Y' in f.read(1): 142 | print( 143 | FAIL + ERROR_PREFIX + "KVM is not properly configured! Please execute the following command:" + ENDC + "\n\n\tsudo su\n\techo 1 > /sys/module/kvm/parameters/ignore_msrs\n") 144 | return False 145 | else: 146 | return True 147 | except: 148 | pass 149 | finally: 150 | f.close() 151 | print(FAIL + ERROR_PREFIX + "KVM is not ready?!" + ENDC) 152 | return False 153 | return True 154 | 155 | 156 | def check_kafl_ini(): 157 | if not os.path.exists(os.path.dirname(sys.argv[0]) + "/kafl.ini"): 158 | from common.config import FuzzerConfiguration 159 | FuzzerConfiguration(skip_args=True).create_initial_config() 160 | print(WARNING + WARNING_PREFIX + "kafl.ini file does not exist. Creating..." + ENDC) 161 | return False 162 | return True 163 | 164 | 165 | def check_qemu_version(config): 166 | if not config.config_values["QEMU_KAFL_LOCATION"] or config.config_values["QEMU_KAFL_LOCATION"] == "": 167 | print(FAIL + ERROR_PREFIX + "QEMU_KAFL_LOCATION is not set in kafl.ini!" + ENDC) 168 | return False 169 | 170 | if not os.path.exists(config.config_values["QEMU_KAFL_LOCATION"]): 171 | print(FAIL + ERROR_PREFIX + "QEMU-PT executable does not exists..." + ENDC) 172 | return False 173 | 174 | output = "" 175 | try: 176 | proc = subprocess.Popen([config.config_values["QEMU_KAFL_LOCATION"], "-version"], stdout=subprocess.PIPE, 177 | stderr=subprocess.PIPE) 178 | output = proc.stdout.readline() 179 | proc.wait() 180 | except: 181 | print(FAIL + ERROR_PREFIX + "Binary is not executable...?" + ENDC) 182 | return False 183 | if not ("QEMU-PT" in output and "(kAFL)" in output): 184 | print(FAIL + ERROR_PREFIX + "Wrong QEMU-PT executable..." + ENDC) 185 | return False 186 | return True 187 | 188 | 189 | def self_check(): 190 | if not check_kafl_ini(): 191 | return False 192 | if not check_if_nativ_lib_compiled(): 193 | return False 194 | if not check_version(): 195 | return False 196 | if not check_packages(): 197 | return False 198 | if not check_vmx_pt(): 199 | return False 200 | return True 201 | 202 | 203 | def post_self_check(config): 204 | if not check_apple_ignore_msrs(config): 205 | return False 206 | if not check_apple_osk(config): 207 | return False 208 | if not check_qemu_version(config): 209 | return False 210 | return True 211 | -------------------------------------------------------------------------------- /common/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import glob 19 | import os 20 | import shutil 21 | import sys 22 | import termios 23 | import time 24 | import tty 25 | import uuid 26 | from shutil import copyfile 27 | 28 | from common.debug import logger 29 | 30 | __author__ = 'Sergej Schumilo' 31 | 32 | 33 | class Singleton(type): 34 | _instances = {} 35 | 36 | def __call__(cls, *args, **kwargs): 37 | if cls not in cls._instances: 38 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 39 | return cls._instances[cls] 40 | 41 | 42 | def atomic_write(filename, data): 43 | tmp_file = "/tmp/" + str(uuid.uuid4()) 44 | f = open(tmp_file, 'wb') 45 | f.write(data) 46 | f.flush() 47 | os.fsync(f.fileno()) 48 | f.close() 49 | shutil.move(tmp_file, filename) 50 | 51 | 52 | def read_binary_file(filename): 53 | payload = "" 54 | f = open(filename, 'rb') 55 | while True: 56 | buf = f.read(1024) 57 | if len(buf) == 0: 58 | break 59 | payload += buf 60 | 61 | f.close() 62 | return payload 63 | 64 | 65 | def find_diffs(data_a, data_b): 66 | first_diff = 0 67 | last_diff = 0 68 | for i in range(min(len(data_a), len(data_b))): 69 | if data_a[i] != data_b: 70 | if first_diff == 0: 71 | first_diff = i 72 | last_diff = i 73 | return first_diff, last_diff 74 | 75 | 76 | def prepare_working_dir(directory_path): 77 | folders = ["/corpus/regular", "/metadata", "/corpus/crash", "/corpus/kasan", "/corpus/timeout", "/bitmaps", 78 | "/imports"] 79 | 80 | project_name = directory_path.split("/")[-1] 81 | 82 | shutil.rmtree(directory_path, ignore_errors=True) 83 | 84 | for path in glob.glob("/dev/shm/kafl_%s_*" % project_name): 85 | os.remove(path) 86 | 87 | if os.path.exists("/dev/shm/kafl_tfilter"): 88 | os.remove("/dev/shm/kafl_tfilter") 89 | 90 | for folder in folders: 91 | os.makedirs(directory_path + folder) 92 | 93 | open(directory_path + "/filter", "wb").close() 94 | open(directory_path + "/page_cache.lock", "wb").close() 95 | open(directory_path + "/page_cache.dump", "wb").close() 96 | open(directory_path + "/page_cache.addr", "wb").close() 97 | 98 | 99 | def copy_seed_files(working_directory, seed_directory): 100 | if len(os.listdir(seed_directory)) == 0: 101 | return False 102 | 103 | if len(os.listdir(working_directory)) == 0: 104 | return True 105 | 106 | i = 0 107 | for (directory, _, files) in os.walk(seed_directory): 108 | for f in files: 109 | path = os.path.join(directory, f) 110 | if os.path.exists(path): 111 | copyfile(path, working_directory + "/imports/" + "seed_%05d" % i) 112 | i += 1 113 | 114 | return True 115 | 116 | 117 | def print_warning(msg): 118 | sys.stdout.write("\033[0;33m\033[1m[WARNING] " + msg + "\033[0m\n") 119 | sys.stdout.flush() 120 | 121 | 122 | def print_fail(msg): 123 | sys.stdout.write("\033[91m\033[1m[FAIL] " + msg + "\033[0m\n") 124 | sys.stdout.flush() 125 | 126 | 127 | def print_pre_exit_msg(num_dots, clrscr=False): 128 | dots = "" 129 | for i in range((num_dots % 3) + 1): 130 | dots += "." 131 | for i in range(3 - len(dots)): 132 | dots += " " 133 | 134 | if clrscr: 135 | print 136 | '\x1b[2J' 137 | print 138 | '\x1b[1;1H' + '\x1b[1;1H' + '\033[0;33m' + "[*] Terminating Slaves" + dots + '\033[0m' + "\n" 139 | 140 | 141 | def print_exit_msg(): 142 | print 143 | '\x1b[2J' + '\x1b[1;1H' + '\033[92m' + "[!] Data saved! Bye!" + '\033[0m' + "\n" 144 | 145 | 146 | def is_float(value): 147 | try: 148 | float(value) 149 | return True 150 | except ValueError: 151 | return False 152 | 153 | 154 | def is_int(value): 155 | try: 156 | int(value) 157 | return True 158 | except ValueError: 159 | return False 160 | 161 | 162 | def getch(): 163 | fd = sys.stdin.fileno() 164 | old_settings = termios.tcgetattr(fd) 165 | try: 166 | tty.setraw(sys.stdin.fileno()) 167 | ch = sys.stdin.read(1) 168 | finally: 169 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 170 | return ch 171 | 172 | 173 | def ask_for_permission(data, text, color='\033[91m'): 174 | ENDC = '\033[0m' 175 | print("Enter " + data + text) 176 | i = 0 177 | print(len(data) * '_'), 178 | while True: 179 | input_char = getch() 180 | 181 | # Check for CTRL+C 182 | if input_char == chr(0x3): 183 | print("") 184 | return False 185 | 186 | # Check for matching character 187 | if (data[i] == input_char): 188 | i += 1 189 | print("\r" + color + data[:i] + ENDC + (len(data) - i) * '_'), 190 | 191 | # Check if we are done here ... 192 | if i == len(data): 193 | break 194 | print("") 195 | return True 196 | 197 | 198 | def json_dumper(obj): 199 | return obj.__dict__ 200 | -------------------------------------------------------------------------------- /debug/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sergej' 2 | -------------------------------------------------------------------------------- /fuzzer/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sergej' 2 | -------------------------------------------------------------------------------- /fuzzer/bitmap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import array 18 | import ctypes 19 | import inspect 20 | import mmap 21 | import os 22 | 23 | from common.safe_syscall import safe_print 24 | 25 | 26 | class GlobalBitmap: 27 | bitmap_native_so = ctypes.CDLL( 28 | os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + '/native/bitmap.so') 29 | bitmap_native_so.are_new_bits_present_no_apply_lut.restype = ctypes.c_uint64 30 | bitmap_native_so.are_new_bits_present_do_apply_lut.restype = ctypes.c_uint64 31 | bitmap_size = None 32 | 33 | def __init__(self, name, config, bitmap_size, read_only=True): 34 | assert (not GlobalBitmap.bitmap_size or GlobalBitmap.bitmap_size == bitmap_size) 35 | GlobalBitmap.bitmap_size = bitmap_size 36 | self.name = name 37 | self.config = config 38 | self.bitmap_size = bitmap_size 39 | self.create_bitmap(name) 40 | self.c_bitmap = (ctypes.c_uint8 * self.bitmap_size).from_buffer(self.bitmap) 41 | self.read_only = read_only 42 | if not read_only: 43 | self.flush_bitmap() 44 | 45 | def flush_bitmap(self): 46 | assert (not self.read_only) 47 | for i in range(self.bitmap_size): 48 | self.c_bitmap[i] = 0 49 | 50 | def create_bitmap(self, name): 51 | self.bitmap_fd = os.open(self.config.argument_values['work_dir'] + "/bitmaps/" + name, 52 | os.O_RDWR | os.O_SYNC | os.O_CREAT) 53 | os.ftruncate(self.bitmap_fd, self.config.config_values['BITMAP_SHM_SIZE']) 54 | self.bitmap = mmap.mmap(self.bitmap_fd, self.bitmap_size, mmap.MAP_SHARED, mmap.PROT_WRITE | mmap.PROT_READ) 55 | 56 | def get_new_byte_and_bit_counts(self, local_bitmap): 57 | c_new_bitmap = local_bitmap.cbuffer 58 | assert c_new_bitmap 59 | if local_bitmap.is_lut_applied(): 60 | result = GlobalBitmap.bitmap_native_so.are_new_bits_present_no_apply_lut(self.c_bitmap, c_new_bitmap, 61 | ctypes.c_uint64(self.bitmap_size)) 62 | else: 63 | result = GlobalBitmap.bitmap_native_so.are_new_bits_present_do_apply_lut(self.c_bitmap, c_new_bitmap, 64 | ctypes.c_uint64(self.bitmap_size)) 65 | local_bitmap.lut_applied = True 66 | 67 | byte_count = result >> 32 68 | bit_count = result & 0xFFFFFFFF 69 | return byte_count, bit_count 70 | 71 | def get_new_byte_and_bit_offsets(self, local_bitmap): 72 | # TODO ensure that local_bitmap doesn't need a copy to increase performance 73 | # when working on a shared version, ensure that all subsequent tests on the bitmap get a properly bucketized bitmap (Trim, Redqueen etc)... 74 | byte_count, bit_count = self.get_new_byte_and_bit_counts(local_bitmap) 75 | 76 | c_new_bitmap = local_bitmap.cbuffer 77 | assert c_new_bitmap 78 | 79 | new_bytes = None 80 | new_bits = None 81 | if byte_count != 0 or bit_count != 0: 82 | new_bytes, new_bits = self.determine_new_bytes(c_new_bitmap) 83 | 84 | # print("byte counts: %d %d %s"%(len(new_bytes), byte_count, repr(new_bytes))) 85 | # print("bit counts: %d %d %s"%(len(new_bits), bit_count, repr(new_bits))) 86 | assert (len(new_bytes) == byte_count) 87 | assert (len(new_bits) == bit_count) 88 | 89 | return new_bytes, new_bits 90 | 91 | @staticmethod 92 | def apply_lut(exec_result): 93 | assert not exec_result.is_lut_applied() 94 | c_new_bitmap = exec_result.cbuffer 95 | GlobalBitmap.bitmap_native_so.apply_bucket_lut(c_new_bitmap, ctypes.c_uint64(exec_result.bitmap_size)) 96 | exec_result.lut_applied = True 97 | 98 | @staticmethod 99 | def all_new_bits_still_set(old_bits, new_bitmap): 100 | assert new_bitmap.is_lut_applied() 101 | c_new_bitmap = new_bitmap.cbuffer 102 | return all([c_new_bitmap[index] == byteval for (index, byteval) in old_bits.items()]) 103 | 104 | def determine_new_bytes(self, exec_result): 105 | new_bytes = {} 106 | new_bits = {} 107 | assert (len(exec_result) == len(self.c_bitmap)) 108 | for index in xrange(self.bitmap_size): 109 | global_byte = self.c_bitmap[index] 110 | local_byte = exec_result[index] 111 | if (global_byte | local_byte) != global_byte: 112 | if global_byte == 0: 113 | new_bytes[index] = local_byte 114 | else: 115 | new_bits[index] = local_byte 116 | return new_bytes, new_bits 117 | 118 | def update_with(self, exec_result): 119 | assert (not self.read_only) 120 | GlobalBitmap.bitmap_native_so.update_global_bitmap(self.c_bitmap, exec_result.cbuffer, 121 | ctypes.c_uint64(self.bitmap_size)) 122 | 123 | 124 | class BitmapStorage: 125 | def __init__(self, config, bitmap_size, prefix, read_only=True): 126 | self.prefix = prefix 127 | self.bitmap_size = bitmap_size 128 | self.normal_bitmap = GlobalBitmap(prefix + "_normal_bitmap", config, self.bitmap_size, read_only) 129 | self.crash_bitmap = GlobalBitmap(prefix + "_crash_bitmap", config, self.bitmap_size, read_only) 130 | self.kasan_bitmap = GlobalBitmap(prefix + "_kasan_bitmap", config, self.bitmap_size, read_only) 131 | self.timeout_bitmap = GlobalBitmap(prefix + "_timeout_bitmap", config, self.bitmap_size, read_only) 132 | 133 | def get_bitmap_for_node_type(self, exit_reason): 134 | if exit_reason == "regular": 135 | return self.normal_bitmap 136 | elif exit_reason == "timeout": 137 | return self.timeout_bitmap 138 | elif exit_reason == "crash": 139 | return self.crash_bitmap 140 | elif exit_reason == "kasan": 141 | return self.kasan_bitmap 142 | else: 143 | assert False, "unexpected node type: {}".format(exit_reason) 144 | 145 | def check_storage_logic(self, exec_result, new_bytes, new_bits): 146 | if exec_result.exit_reason == "regular" and (new_bits or new_bytes): 147 | return True 148 | elif new_bytes: 149 | return True 150 | return False 151 | 152 | def should_send_to_master(self, exec_result): 153 | relevant_bitmap = self.get_bitmap_for_node_type(exec_result.exit_reason) 154 | new_bytes, new_bits = relevant_bitmap.get_new_byte_and_bit_counts(exec_result) 155 | return self.check_storage_logic(exec_result, new_bytes, new_bits) 156 | 157 | def should_store_in_queue(self, exec_result): 158 | relevant_bitmap = self.get_bitmap_for_node_type(exec_result.exit_reason) 159 | new_bytes, new_bits = relevant_bitmap.get_new_byte_and_bit_offsets(exec_result) 160 | accepted = self.check_storage_logic(exec_result, new_bytes, new_bits) 161 | if accepted: 162 | relevant_bitmap.update_with(exec_result) 163 | 164 | return accepted, new_bytes, new_bits 165 | -------------------------------------------------------------------------------- /fuzzer/communicator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import msgpack 19 | import select 20 | from multiprocessing.connection import Listener, Client 21 | 22 | MSG_HELLO = 0 23 | MSG_NEW_TASK = 1 24 | MSG_QUEUE_STATUS = 2 25 | MSG_TASK_RESULTS = 3 26 | MSG_NEW_INPUT = 4 27 | 28 | 29 | class ServerConnection: 30 | 31 | def __init__(self, config): 32 | 33 | Listener.fileno = lambda self: self._listener._socket.fileno() 34 | 35 | self.address = config.argument_values["work_dir"] + "/slave_socket" 36 | self.listener = Listener(self.address, 'AF_UNIX') 37 | self.clients = [self.listener] 38 | 39 | def wait(self): 40 | results = [] 41 | print 42 | "selecting" 43 | r, w, e = select.select(self.clients, (), ()) 44 | for sock_ready in r: 45 | if sock_ready == self.listener: 46 | print 47 | "accepting new client" 48 | c = self.listener.accept() 49 | self.clients.append(c) 50 | else: 51 | try: 52 | msg = sock_ready.recv_bytes() 53 | msg = msgpack.unpackb(msg) 54 | results.append((sock_ready, msg)) 55 | # print "received {}".format(msg) 56 | except EOFError: 57 | print 58 | "closing" 59 | sock_ready.close() 60 | self.clients.remove(sock_ready) 61 | return results 62 | 63 | def send_task(self, client, task_data): 64 | client.send_bytes(msgpack.packb({"type": MSG_NEW_TASK, "task": task_data})) 65 | 66 | def queue_status(self, client): 67 | pass 68 | 69 | 70 | class ClientConnection: 71 | def __init__(self, id, config): 72 | self.id = id 73 | self.address = config.argument_values["work_dir"] + "/slave_socket" 74 | self.sock = self.connect() 75 | self.send_hello() 76 | 77 | def connect(self): 78 | sock = Client(self.address, 'AF_UNIX') 79 | return sock 80 | 81 | def recv(self): 82 | data = self.sock.recv_bytes() 83 | return msgpack.unpackb(data) 84 | 85 | def send_hello(self): 86 | print 87 | "sending CLIENT_HELLO" 88 | self.sock.send_bytes(msgpack.packb({"type": MSG_HELLO, "client_id": self.id})) 89 | 90 | def send_new_input(self, data, bitmap, info): 91 | self.sock.send_bytes( 92 | msgpack.packb({"type": MSG_NEW_INPUT, "input": {"payload": data, "bitmap": bitmap, "info": info}})) 93 | 94 | def send_task_performed(self, node_id, results, new_payload): 95 | self.sock.send_bytes(msgpack.packb( 96 | {"type": MSG_TASK_RESULTS, "node_id": node_id, "results": results, "new_payload": new_payload})) 97 | 98 | def send_slave_status(self): 99 | pass 100 | 101 | def send_terminated(self): 102 | pass 103 | -------------------------------------------------------------------------------- /fuzzer/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import multiprocessing 19 | import signal 20 | import time 21 | 22 | from common.config import FuzzerConfiguration 23 | from common.debug import enable_logging 24 | from common.self_check import post_self_check 25 | from common.util import prepare_working_dir, print_fail, ask_for_permission, print_warning, copy_seed_files 26 | from process.master import MasterProcess 27 | from process.slave import slave_loader 28 | 29 | 30 | def start(): 31 | config = FuzzerConfiguration() 32 | 33 | if not post_self_check(config): 34 | return -1 35 | 36 | if config.argument_values['v']: 37 | enable_logging(config.argument_values["work_dir"]) 38 | 39 | num_processes = config.argument_values['p'] 40 | 41 | if not config.argument_values['Purge']: 42 | if ask_for_permission("PURGE", " to wipe old workspace:"): 43 | print_warning("Wiping old workspace...") 44 | time.sleep(2) 45 | else: 46 | print_fail("Aborting...") 47 | return 0 48 | 49 | prepare_working_dir(config.argument_values['work_dir']) 50 | 51 | if not copy_seed_files(config.argument_values['work_dir'], config.argument_values['seed_dir']): 52 | print_fail("Seed directory is empty...") 53 | return 1 54 | 55 | master = MasterProcess(config) 56 | 57 | slaves = [] 58 | for i in range(num_processes): 59 | print 60 | "fuzzing process {}".format(i) 61 | slaves.append(multiprocessing.Process(name='SLAVE' + str(i), target=slave_loader, args=(i,))) 62 | slaves[i].start() 63 | 64 | try: 65 | master.loop() 66 | except KeyboardInterrupt: 67 | pass 68 | 69 | signal.signal(signal.SIGINT, signal.SIG_IGN) 70 | 71 | counter = 0 72 | # print_pre_exit_msg(counter, clrscr=True) 73 | for slave in slaves: 74 | while True: 75 | counter += 1 76 | # print_pre_exit_msg(counter) 77 | slave.join(timeout=0.25) 78 | if not slave.is_alive(): 79 | break 80 | # print_exit_msg() 81 | return 0 82 | -------------------------------------------------------------------------------- /fuzzer/native/bitmap.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | static const uint8_t bucket_lut[256] = { 24 | [0] = 0, 25 | [1] = 1, 26 | [2] = 2, 27 | [3] = 4, 28 | [4 ... 7] = 8, 29 | [8 ... 15] = 16, 30 | [16 ... 31] = 32, 31 | [32 ... 127] = 64, 32 | [128 ... 255] = 128 33 | }; 34 | 35 | static void con() __attribute__((constructor)); 36 | void init() { 37 | 38 | } 39 | 40 | /** 41 | * @brief Checks if two bitmaps differ. 42 | * @param bitmap The bucket bitmap. 43 | * A zero bit indicates that the specific bucket of the given byte is free. 44 | * @param new_bitmap A bitmap from a recent run. 45 | * Each byte value of this map is assigned to one of 9 buckets. 46 | * @param bitmap_size The length of both bitmaps. 47 | * @return true if the maps differ after "bucketing". 48 | */ 49 | uint64_t are_new_bits_present_do_apply_lut(uint8_t* bitmap, uint8_t* new_bitmap, uint64_t bitmap_size) { 50 | uint64_t bit_count = 0; 51 | uint64_t byte_count = 0; 52 | for (uint64_t i = 0; i < bitmap_size; i++) { 53 | uint8_t a = bucket_lut[new_bitmap[i]]; 54 | new_bitmap[i] = a; //THIS ONE is not availble below at no_apply_lut 55 | if( (a | bitmap[i]) != bitmap[i] ) { 56 | if (bitmap[i]==0){ 57 | byte_count++; 58 | } else { 59 | bit_count++; 60 | } 61 | } 62 | } 63 | return (uint64_t)((byte_count << 32) + (bit_count)); 64 | } 65 | 66 | uint64_t are_new_bits_present_no_apply_lut(uint8_t* bitmap, uint8_t* new_bitmap, uint64_t bitmap_size) { 67 | uint64_t bit_count = 0; 68 | uint64_t byte_count = 0; 69 | 70 | for (uint64_t i = 0; i < bitmap_size; i++) { 71 | uint8_t a = new_bitmap[i]; 72 | if( (a | bitmap[i]) != bitmap[i] ) { 73 | if (bitmap[i]==0){ 74 | byte_count++; 75 | } else { 76 | bit_count++; 77 | } 78 | } 79 | } 80 | return (uint64_t)((byte_count << 32) + (bit_count)); 81 | } 82 | 83 | void update_global_bitmap(uint8_t* bitmap, uint8_t* new_bitmap, uint64_t bitmap_size) { 84 | for (uint64_t i = 0; i < bitmap_size; i++) { 85 | bitmap[i] |= new_bitmap[i]; 86 | } 87 | } 88 | 89 | void apply_bucket_lut(uint8_t * bitmap, uint64_t bitmap_size) { 90 | for (uint64_t i = 0; i < bitmap_size; i++) { 91 | bitmap[i] = bucket_lut[bitmap[i]]; 92 | } 93 | } 94 | 95 | /* AFL source code incoming... */ 96 | uint8_t could_be_bitflip(uint32_t xor_val) { 97 | 98 | uint32_t sh = 0; 99 | 100 | if (!xor_val) return 1; 101 | 102 | /* Shift left until first bit set. */ 103 | 104 | while (!(xor_val & 1)) { sh++; xor_val >>= 1; } 105 | 106 | /* 1-, 2-, and 4-bit patterns are OK anywhere. */ 107 | 108 | if (xor_val == 1 || xor_val == 3 || xor_val == 15) return 1; 109 | 110 | /* 8-, 16-, and 32-bit patterns are OK only if shift factor is 111 | divisible by 8, since that's the stepover for these ops. */ 112 | 113 | if (sh & 7) return 0; 114 | 115 | if (xor_val == 0xff || xor_val == 0xffff || xor_val == 0xffffffff) 116 | return 1; 117 | 118 | return 0; 119 | 120 | } 121 | 122 | /* AFL source code incoming... */ 123 | #define SWAP16(_x) ({ \ 124 | uint16_t _ret = (_x); \ 125 | (uint16_t)((_ret << 8) | (_ret >> 8)); \ 126 | }) 127 | 128 | /* AFL source code incoming... */ 129 | #define SWAP32(_x) ({ \ 130 | uint32_t _ret = (_x); \ 131 | (uint32_t)((_ret << 24) | (_ret >> 24) | \ 132 | ((_ret << 8) & 0x00FF0000) | \ 133 | ((_ret >> 8) & 0x0000FF00)); \ 134 | }) 135 | 136 | /* AFL source code incoming... */ 137 | uint8_t could_be_arith(uint32_t old_val, uint32_t new_val, uint8_t blen, uint8_t ARITH_MAX) { 138 | 139 | uint32_t i, ov = 0, nv = 0, diffs = 0; 140 | 141 | if (old_val == new_val) return 1; 142 | 143 | /* See if one-byte adjustments to any byte could produce this result. */ 144 | 145 | for (i = 0; i < blen; i++) { 146 | 147 | uint8_t a = old_val >> (8 * i), 148 | b = new_val >> (8 * i); 149 | 150 | if (a != b) { diffs++; ov = a; nv = b; } 151 | 152 | } 153 | 154 | /* If only one byte differs and the values are within range, return 1. */ 155 | 156 | if (diffs == 1) { 157 | 158 | if ((uint8_t)(ov - nv) <= ARITH_MAX || 159 | (uint8_t)(nv - ov) <= ARITH_MAX) return 1; 160 | 161 | } 162 | 163 | if (blen == 1) return 0; 164 | 165 | /* See if two-byte adjustments to any byte would produce this result. */ 166 | 167 | diffs = 0; 168 | 169 | for (i = 0; i < blen / 2; i++) { 170 | 171 | uint16_t a = old_val >> (16 * i), 172 | b = new_val >> (16 * i); 173 | 174 | if (a != b) { diffs++; ov = a; nv = b; } 175 | 176 | } 177 | 178 | /* If only one word differs and the values are within range, return 1. */ 179 | 180 | if (diffs == 1) { 181 | 182 | if ((uint16_t)(ov - nv) <= ARITH_MAX || 183 | (uint16_t)(nv - ov) <= ARITH_MAX) return 1; 184 | 185 | ov = SWAP16(ov); nv = SWAP16(nv); 186 | 187 | if ((uint16_t)(ov - nv) <= ARITH_MAX || 188 | (uint16_t)(nv - ov) <= ARITH_MAX) return 1; 189 | 190 | } 191 | 192 | /* Finally, let's do the same thing for dwords. */ 193 | 194 | if (blen == 4) { 195 | 196 | if ((uint32_t)(old_val - new_val) <= ARITH_MAX || 197 | (uint32_t)(new_val - old_val) <= ARITH_MAX) return 1; 198 | 199 | new_val = SWAP32(new_val); 200 | old_val = SWAP32(old_val); 201 | 202 | if ((uint32_t)(old_val - new_val) <= ARITH_MAX || 203 | (uint32_t)(new_val - old_val) <= ARITH_MAX) return 1; 204 | 205 | } 206 | 207 | return 0; 208 | 209 | } 210 | 211 | typedef int8_t s8; 212 | typedef int16_t s16; 213 | typedef int32_t s32; 214 | typedef int64_t s64; 215 | 216 | #define INTERESTING_8 \ 217 | -128, /* Overflow signed 8-bit when decremented */ \ 218 | -1, /* */ \ 219 | 0, /* */ \ 220 | 1, /* */ \ 221 | 16, /* One-off with common buffer size */ \ 222 | 32, /* One-off with common buffer size */ \ 223 | 64, /* One-off with common buffer size */ \ 224 | 100, /* One-off with common buffer size */ \ 225 | 127 /* Overflow signed 8-bit when incremented */ 226 | 227 | #define INTERESTING_16 \ 228 | -32768, /* Overflow signed 16-bit when decremented */ \ 229 | -129, /* Overflow signed 8-bit */ \ 230 | 128, /* Overflow signed 8-bit */ \ 231 | 255, /* Overflow unsig 8-bit when incremented */ \ 232 | 256, /* Overflow unsig 8-bit */ \ 233 | 512, /* One-off with common buffer size */ \ 234 | 1000, /* One-off with common buffer size */ \ 235 | 1024, /* One-off with common buffer size */ \ 236 | 4096, /* One-off with common buffer size */ \ 237 | 32767 /* Overflow signed 16-bit when incremented */ 238 | 239 | #define INTERESTING_32 \ 240 | -2147483648LL, /* Overflow signed 32-bit when decremented */ \ 241 | -100663046, /* Large negative number (endian-agnostic) */ \ 242 | -32769, /* Overflow signed 16-bit */ \ 243 | 32768, /* Overflow signed 16-bit */ \ 244 | 65535, /* Overflow unsig 16-bit when incremented */ \ 245 | 65536, /* Overflow unsig 16 bit */ \ 246 | 100663045, /* Large positive number (endian-agnostic) */ \ 247 | 2147483647 /* Overflow signed 32-bit when incremented */ 248 | 249 | 250 | static s8 interesting_8[] = { INTERESTING_8 }; 251 | static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 }; 252 | static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 }; 253 | 254 | 255 | uint8_t could_be_interest(uint32_t old_val, uint32_t new_val, uint8_t blen, uint8_t check_le) { 256 | 257 | uint32_t i, j; 258 | 259 | if (old_val == new_val) return 1; 260 | 261 | /* See if one-byte insertions from interesting_8 over old_val could 262 | produce new_val. */ 263 | 264 | for (i = 0; i < blen; i++) { 265 | 266 | for (j = 0; j < sizeof(interesting_8); j++) { 267 | 268 | uint32_t tval = (old_val & ~(0xff << (i * 8))) | 269 | (((uint8_t)interesting_8[j]) << (i * 8)); 270 | 271 | if (new_val == tval) return 1; 272 | 273 | } 274 | 275 | } 276 | 277 | /* Bail out unless we're also asked to examine two-byte LE insertions 278 | as a preparation for BE attempts. */ 279 | 280 | if (blen == 2 && !check_le) return 0; 281 | 282 | /* See if two-byte insertions over old_val could give us new_val. */ 283 | 284 | for (i = 0; i < blen - 1; i++) { 285 | 286 | for (j = 0; j < sizeof(interesting_16) / 2; j++) { 287 | 288 | uint32_t tval = (old_val & ~(0xffff << (i * 8))) | 289 | (((uint16_t)interesting_16[j]) << (i * 8)); 290 | 291 | if (new_val == tval) return 1; 292 | 293 | /* Continue here only if blen > 2. */ 294 | 295 | if (blen > 2) { 296 | 297 | tval = (old_val & ~(0xffff << (i * 8))) | 298 | (SWAP16(interesting_16[j]) << (i * 8)); 299 | 300 | if (new_val == tval) return 1; 301 | 302 | } 303 | 304 | } 305 | 306 | } 307 | 308 | if (blen == 4 && check_le) { 309 | 310 | /* See if four-byte insertions could produce the same result 311 | (LE only). */ 312 | 313 | for (j = 0; j < sizeof(interesting_32) / 4; j++) 314 | if (new_val == (uint32_t)interesting_32[j]) return 1; 315 | 316 | } 317 | 318 | return 0; 319 | 320 | } 321 | -------------------------------------------------------------------------------- /fuzzer/node.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import zlib 18 | 19 | import mmh3 20 | import msgpack 21 | 22 | from common.config import FuzzerConfiguration 23 | from common.util import read_binary_file, atomic_write 24 | 25 | 26 | class QueueNode: 27 | NextID = 1 28 | 29 | def __init__(self, payload, bitmap, node_struct, write=True): 30 | self.node_struct = node_struct 31 | 32 | self.set_id(QueueNode.NextID, write=False) 33 | 34 | QueueNode.NextID += 1 35 | 36 | self.set_payload(payload, write=False) 37 | self.set_bitmap(bitmap) 38 | self.update_file(write) 39 | 40 | @staticmethod 41 | def get_metadata(id): 42 | return msgpack.unpackb(read_binary_file(QueueNode.__get_metadata_filename(id))) 43 | 44 | @staticmethod 45 | def get_payload(exitreason, id): 46 | return read_binary_file(QueueNode.__get_payload_filename(exitreason, id)) 47 | 48 | def __get_bitmap_filename(self): 49 | workdir = FuzzerConfiguration().argument_values['work_dir'] 50 | filename = "/bitmaps/payload_%05d.gzip" % (self.get_id()) 51 | return workdir + filename 52 | 53 | @staticmethod 54 | def __get_payload_filename(exit_reason, id): 55 | workdir = FuzzerConfiguration().argument_values['work_dir'] 56 | filename = "/corpus/%s/payload_%05d" % (exit_reason, id) 57 | return workdir + filename 58 | 59 | @staticmethod 60 | def __get_metadata_filename(id): 61 | workdir = FuzzerConfiguration().argument_values['work_dir'] 62 | return workdir + "/metadata/node_%05d" % id 63 | 64 | def update_file(self, write=True): 65 | if write: 66 | self.write_metadata() 67 | self.dirty = False 68 | else: 69 | self.dirty = True 70 | 71 | def set_bitmap(self, bitmap): 72 | atomic_write(self.__get_bitmap_filename(), zlib.compress(bitmap)) 73 | 74 | def get_bitmap(self): 75 | return zlib.decompress(read_binary_file(self.__get_bitmap_filename())) 76 | 77 | def write_metadata(self): 78 | return atomic_write(QueueNode.__get_metadata_filename(self.get_id()), msgpack.packb(self.node_struct)) 79 | 80 | def load_metadata(self): 81 | QueueNode.get_metadata(self.id) 82 | 83 | @staticmethod 84 | # will be used both for the final update and the intermediate update in the statelogic. Needs to work in both occasions! 85 | # That means it needs to be able to apply an update to another update as well as the final meta data 86 | # This function must leave new_data unchanged, but may change old_data 87 | def apply_metadata_update(old_data, new_data): 88 | new_data = new_data.copy() # if we remove keys deeper than attention_execs and attention_secs, we need a deep copy 89 | old_data["attention_execs"] = old_data.get("attention_execs", 0) + new_data["attention_execs"] 90 | old_data["attention_secs"] = old_data.get("attention_secs", 0) + new_data["attention_secs"] 91 | 92 | for key in ["state_time_initial", "state_time_havoc", "state_time_grimoire", "state_time_grimoire_inference", 93 | "state_time_redqueen"]: 94 | old_data[key] = old_data.get(key, 0) + new_data[key] 95 | del new_data[key] 96 | 97 | del new_data["attention_execs"] 98 | del new_data["attention_secs"] 99 | old_data.update(new_data) 100 | return old_data 101 | 102 | def update_metadata(self, delta, write=True): 103 | self.node_struct = QueueNode.apply_metadata_update(self.node_struct, delta) 104 | self.update_file(write) 105 | 106 | def set_payload(self, payload, write=True): 107 | self.set_payload_len(len(payload), write=False) 108 | self.set_payload_hash(mmh3.hash(payload), write) 109 | atomic_write(QueueNode.__get_payload_filename(self.get_exit_reason(), self.get_id()), payload) 110 | 111 | def load_payload(self): 112 | QueueNode.get_payload(self.get_exit_reason(), self.get_id()) 113 | 114 | def get_payload_len(self): 115 | return self.node_struct["payload_len"] 116 | 117 | def set_payload_len(self, val, write=True): 118 | self.node_struct["payload_len"] = val 119 | self.update_file(write) 120 | 121 | def get_id(self): 122 | return self.node_struct["id"] 123 | 124 | def set_id(self, val, write=True): 125 | self.node_struct["id"] = val 126 | self.update_file(write) 127 | 128 | def get_payload_hash(self): 129 | return self.node_struct["payload_hash"] 130 | 131 | def set_payload_hash(self, val, write=True): 132 | self.node_struct["payload_hash"] = val 133 | self.update_file(write) 134 | 135 | def get_new_byte_count(self): 136 | return self.node_struct["new_byte_count"] 137 | 138 | def set_new_byte_count(self, val, write=True): 139 | self.node_struct["new_byte_count"] = val 140 | self.update_file(write) 141 | 142 | def get_new_bit_count(self): 143 | return self.node_struct["new_bit_count"] 144 | 145 | def set_new_bit_count(self, val, write=True): 146 | self.node_struct["new_bit_count"] = val 147 | self.update_file(write) 148 | 149 | def get_new_bytes(self): 150 | return self.node_struct["new_bytes"] 151 | 152 | def set_new_bytes(self, val, write=True): 153 | self.node_struct["new_bytes"] = val 154 | self.update_file(write) 155 | 156 | def get_new_bits(self): 157 | return self.node_struct["new_bits"] 158 | 159 | def clear_fav_bits(self, write=True): 160 | self.node_struct["fav_bits"] = {} 161 | self.update_file(write) 162 | 163 | def get_fav_bits(self): 164 | return self.node_struct["fav_bits"] 165 | 166 | def add_fav_bit(self, index, write=True): 167 | self.node_struct["fav_bits"][index] = 0 168 | self.update_file(write) 169 | 170 | def remove_fav_bit(self, index, write=True): 171 | assert index in self.node_struct["fav_bits"] 172 | self.node_struct["fav_bits"].pop(index) 173 | self.update_file(write) 174 | 175 | def set_new_bits(self, val, write=True): 176 | self.node_struct["new_bits"] = val 177 | self.update_file(write) 178 | 179 | def get_level(self): 180 | return self.node_struct["level"] 181 | 182 | def set_level(self, val, write=True): 183 | self.node_struct["level"] = val 184 | self.update_file(write) 185 | 186 | def get_favorite(self): 187 | return len(self.node_struct["fav_bits"]) > 0 188 | 189 | def get_performance(self): 190 | return self.node_struct["info"]["performance"] 191 | 192 | def set_performance(self, val, write=True): 193 | self.node_struct["info"]["performance"] = val 194 | self.update_file(write) 195 | 196 | def get_state(self): 197 | return self.node_struct["state"]["name"] 198 | 199 | def set_state(self, val, write=True): 200 | self.node_struct["state"]["name"] = val 201 | self.update_file(write) 202 | 203 | def get_exit_reason(self): 204 | return self.node_struct["info"]["exit_reason"] 205 | 206 | def set_exit_reason(self, val, write=True): 207 | self.node_struct["info"]["exit_reason"] = val 208 | self.update_file(write) 209 | 210 | def get_fav_factor(self): 211 | return self.node_struct["fav_factor"] 212 | 213 | def set_fav_factor(self, val, write=True): 214 | self.node_struct["fav_factor"] = val 215 | self.update_file(write) 216 | -------------------------------------------------------------------------------- /fuzzer/process/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sergej' 2 | -------------------------------------------------------------------------------- /fuzzer/process/master.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import glob 19 | import os 20 | 21 | from common.debug import log_master 22 | from common.util import read_binary_file 23 | from fuzzer.communicator import ServerConnection, MSG_TASK_RESULTS, MSG_NEW_INPUT, MSG_HELLO 24 | from fuzzer.queue import InputQueue 25 | from fuzzer.scheduler import Scheduler 26 | from fuzzer.statistics import MasterStatistics 27 | from fuzzer.technique.helper import random_string 28 | from fuzzer.technique.redqueen.cmp import enable_hammering 29 | 30 | 31 | class MasterProcess: 32 | 33 | def __init__(self, config): 34 | self.config = config 35 | self.comm = ServerConnection(self.config) 36 | 37 | self.scheduler = Scheduler() 38 | self.statistics = MasterStatistics(self.config) 39 | self.queue = InputQueue(self.config, self.scheduler, self.statistics) 40 | 41 | self.skip_zero = self.config.argument_values['s'] 42 | self.refresh_rate = self.config.config_values['UI_REFRESH_RATE'] 43 | self.use_effector_map = self.config.argument_values['d'] 44 | self.arith_max = self.config.config_values["ARITHMETIC_MAX"] 45 | 46 | self.mode_fix_checksum = self.config.argument_values["fix_hashes"] 47 | 48 | if not self.config.argument_values['D']: 49 | self.use_effector_map = False 50 | 51 | if self.config.argument_values['hammer_jmp_tables']: 52 | enable_hammering() 53 | 54 | print("Master PID: %d\n", os.getpid()) 55 | log_master("Use effector maps: " + str(self.use_effector_map)) 56 | 57 | def get_task(self): 58 | imports = glob.glob(self.config.argument_values['work_dir'] + "/imports/*") 59 | if imports: 60 | path = imports.pop() 61 | payload = read_binary_file(path) 62 | os.remove(path) 63 | return {"payload": payload, "type": "import"} 64 | elif self.queue.has_inputs(): 65 | node = self.queue.get_next() 66 | return {"type": "node", "nid": node.get_id()} 67 | else: 68 | return {"payload": random_string(), "type": "import"} 69 | 70 | def loop(self): 71 | while True: 72 | for conn, msg in self.comm.wait(): 73 | if msg["type"] == MSG_TASK_RESULTS: 74 | # print repr(msg) 75 | if msg["node_id"]: 76 | results = msg["results"] 77 | if results: 78 | node = self.queue.get_node_by_id(msg["node_id"]) 79 | node.update_metadata(results) 80 | new_payload = msg["new_payload"] 81 | if new_payload: 82 | node.set_payload(new_payload) 83 | self.comm.send_task(conn, self.get_task()) 84 | elif msg["type"] == MSG_NEW_INPUT: 85 | node_struct = {"info": msg["input"]["info"], "state": {"name": "initial"}} 86 | self.queue.maybe_insert_node(msg["input"]["payload"], msg["input"]["bitmap"], node_struct) 87 | # print "new input: {}".format(repr(msg["input"]["payload"])) 88 | elif msg["type"] == MSG_HELLO: 89 | print 90 | "got CLIENT_HELLO" 91 | self.comm.send_task(conn, self.get_task()) 92 | else: 93 | raise "unknown message type {}".format(msg) 94 | -------------------------------------------------------------------------------- /fuzzer/process/slave.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import os 19 | import psutil 20 | import time 21 | import traceback 22 | 23 | from common.config import FuzzerConfiguration 24 | from common.debug import log_slave, configure_log_prefix 25 | from common.qemu import qemu 26 | from common.safe_syscall import safe_print 27 | from common.util import read_binary_file, atomic_write 28 | from fuzzer.bitmap import BitmapStorage 29 | from fuzzer.bitmap import GlobalBitmap 30 | from fuzzer.communicator import ClientConnection, MSG_NEW_TASK, MSG_QUEUE_STATUS 31 | from fuzzer.node import QueueNode 32 | from fuzzer.state_logic import FuzzingStateLogic 33 | from fuzzer.statistics import SlaveStatistics 34 | 35 | 36 | def slave_loader(slave_id): 37 | log_slave("PID: " + str(os.getpid()), slave_id) 38 | # sys.stdout = open("slave_%d.out"%slave_id, "w") 39 | config = FuzzerConfiguration() 40 | if config.argument_values["cpu_affinity"]: 41 | psutil.Process().cpu_affinity([config.argument_values["cpu_affinity"]]) 42 | else: 43 | psutil.Process().cpu_affinity([slave_id]) 44 | connection = ClientConnection(slave_id, config) 45 | slave_process = SlaveProcess(slave_id, config, connection) 46 | try: 47 | slave_process.loop() 48 | except KeyboardInterrupt: 49 | slave_process.conn.send_terminated() 50 | log_slave("Killed!", slave_id) 51 | 52 | 53 | num_fucky = 0 54 | 55 | 56 | class SlaveProcess: 57 | 58 | def __init__(self, slave_id, config, connection, auto_reload=False): 59 | self.config = config 60 | self.slave_id = slave_id 61 | self.q = qemu(self.slave_id, self.config) 62 | self.q.start(verbose=False) 63 | print 64 | "started qemu" 65 | self.statistics = SlaveStatistics(self.slave_id, self.config) 66 | self.logic = FuzzingStateLogic(self, self.config) 67 | self.conn = connection 68 | 69 | self.bitmap_storage = BitmapStorage(self.config, self.config.config_values['BITMAP_SHM_SIZE'], "master") 70 | configure_log_prefix("%.2d" % slave_id) 71 | 72 | def handle_server_msg(self, msg): 73 | if msg["type"] == MSG_NEW_TASK: 74 | return self.handle_task(msg) 75 | if msg["type"] == MSG_QUEUE_STATUS: 76 | return self.handle_queue_status(msg) 77 | raise "unknown message type {}".format(msg) 78 | 79 | def handle_task(self, msg): 80 | if msg["task"]["type"] == "import": 81 | meta_data = {"state": {"name": "import"}} 82 | payload = msg["task"]["payload"] 83 | elif msg["task"]["type"] == "node": 84 | meta_data = QueueNode.get_metadata(msg["task"]["nid"]) 85 | payload = QueueNode.get_payload(meta_data["info"]["exit_reason"], meta_data["id"]) 86 | print 87 | "slave %d got task %d %s" % (self.slave_id, meta_data.get("node", {}).get("id", -1), repr(meta_data)) 88 | self.statistics.event_task(msg["task"]) 89 | results, new_payload = self.logic.process(payload, meta_data) 90 | node_id = None 91 | if new_payload != payload: 92 | default_info = {"method": "validate_bits", "parent": meta_data["id"]} 93 | if self.validate_bits(new_payload, meta_data, default_info): 94 | print("VALIDATE BITS OK") 95 | else: 96 | print("VALIDATE BITS FAILED BUG IN TRANSFORMATION!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 97 | # assert False 98 | if results: 99 | node_id = meta_data["id"] 100 | self.conn.send_task_performed(node_id, results, new_payload) 101 | # print "performed task" 102 | 103 | def handle_queue_status(self, msg): 104 | pass 105 | 106 | def loop(self): 107 | while True: 108 | # print "client waiting...." 109 | msg = self.conn.recv() 110 | # print "got %s"%repr(msg) 111 | self.handle_server_msg(msg) 112 | 113 | def validate(self, data, old_array): 114 | self.q.set_payload(data) 115 | self.statistics.event_exec() 116 | new_bitmap = self.q.send_payload().apply_lut() 117 | new_array = new_bitmap.copy_to_array() 118 | if new_array == old_array: 119 | print("Validate OK") 120 | return True, new_bitmap 121 | else: 122 | for i in xrange(new_bitmap.bitmap_size): 123 | if old_array[i] != new_array[i]: 124 | safe_print("found fucky bit %d (%d vs %d)" % (i, old_array[i], new_array[i])) 125 | # assert(False) 126 | 127 | print("VALIDATE FAILED, Not returning a bitmap!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 128 | return False, None 129 | 130 | def validate_bits(self, data, old_node, default_info): 131 | new_bitmap, _ = self.execute_with_bitmap(data, default_info) 132 | # handle non-det inputs 133 | if new_bitmap is None: 134 | return False 135 | old_bits = old_node["new_bytes"].copy() 136 | old_bits.update(old_node["new_bits"]) 137 | return GlobalBitmap.all_new_bits_still_set(old_bits, new_bitmap) 138 | 139 | def validate_bytes(self, data, old_node, default_info): 140 | new_bitmap, _ = self.execute_with_bitmap(data, default_info) 141 | # handle non-det inputs 142 | if new_bitmap is None: 143 | return False 144 | old_bits = old_node["new_bytes"].copy() 145 | return GlobalBitmap.all_new_bits_still_set(old_bits, new_bitmap) 146 | 147 | def execute_redqueen(self, data): 148 | self.statistics.event_exec_redqueen() 149 | return self.q.execute_in_redqueen_mode(data, debug_mode=False) 150 | 151 | def execute_with_bitmap(self, data, info): 152 | bitmap, new_input = self.__execute(data, info) 153 | return bitmap, new_input 154 | 155 | def execute(self, data, info): 156 | bitmap, new_input = self.__execute(data, info) 157 | return new_input 158 | 159 | def __send_to_master(self, data, execution_res, info): 160 | info["time"] = time.time() 161 | info["exit_reason"] = execution_res.exit_reason 162 | info["performance"] = execution_res.performance 163 | if self.conn is not None: 164 | self.conn.send_new_input(data, execution_res.copy_to_array(), info) 165 | 166 | def check_fuckyness_and_store_trace(self, data): 167 | global num_fucky 168 | exec_res = self.q.send_payload() 169 | hash = exec_res.hash() 170 | trace1 = read_binary_file(self.config.argument_values['work_dir'] + "/pt_trace_dump_%d" % self.slave_id) 171 | exec_res = self.q.send_payload() 172 | if (hash != exec_res.hash()): 173 | safe_print("found fucky bits, dumping!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 174 | num_fucky += 1 175 | trace_folder = self.config.argument_values['work_dir'] + "/traces/fucky_%d_%d" % (num_fucky, self.slave_id); 176 | os.makedirs(trace_folder) 177 | atomic_write(trace_folder + "/input", data) 178 | atomic_write(trace_folder + "/trace_a", trace1) 179 | trace2 = read_binary_file(self.config.argument_values["work_dir"] + "/pt_trace_dump_%d" % self.slave_id) 180 | atomic_write(trace_folder + "/trace_b", trace2) 181 | return exec_res 182 | 183 | def __execute(self, data, info): 184 | self.statistics.event_exec() 185 | self.q.set_payload(data) 186 | if False: # Do not emit tracefiles on broken executions 187 | exec_res = self.check_fuckyness_and_store_trace(data) 188 | else: 189 | exec_res = self.q.send_payload() 190 | 191 | is_new_input = self.bitmap_storage.should_send_to_master(exec_res) 192 | crash = self.execution_exited_abnormally() # we do not want to validate timeouts and crashes as they tend to be nondeterministic 193 | if is_new_input: 194 | if not crash: 195 | assert exec_res.is_lut_applied() 196 | bitmap_array = exec_res.copy_to_array() 197 | valid, exec_res = self.validate(data, bitmap_array) 198 | if crash or valid: 199 | self.__send_to_master(data, exec_res, info) 200 | return exec_res, is_new_input 201 | 202 | def execution_exited_abnormally(self): 203 | return self.q.crashed or self.q.timeout or self.q.kasan 204 | 205 | # Todo: Fixme 206 | def __restart_vm(self): 207 | return True 208 | if self.comm.slave_termination.value: 209 | return False 210 | self.comm.reload_semaphore.acquire() 211 | try: 212 | # raise Exception("!") 213 | # QEMU is full of memory leaks...fixing it that way... 214 | if self.soft_reload_counter >= 32: 215 | self.soft_reload_counter = 0 216 | raise Exception("...") 217 | self.q.soft_reload() 218 | self.soft_reload_counter += 1 219 | except: 220 | log_slave("restart failed %s" % traceback.format_exc(), self.slave_id) 221 | while True: 222 | self.q.__del__() 223 | self.q = qemu(self.slave_id, self.config) 224 | if self.q.start(): 225 | break 226 | else: 227 | time.sleep(0.5) 228 | log_slave("Fail Reload", self.slave_id) 229 | self.comm.reload_semaphore.release() 230 | self.q.set_tick_timeout_treshold(self.stage_tick_treshold * self.timeout_tick_factor) 231 | if self.comm.slave_termination.value: 232 | return False 233 | return True 234 | -------------------------------------------------------------------------------- /fuzzer/queue.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | from common.execution_result import ExecutionResult 18 | from common.safe_syscall import safe_print 19 | from fuzzer.bitmap import BitmapStorage 20 | from fuzzer.node import QueueNode 21 | from fuzzer.scheduler import GrimoireScheduler 22 | 23 | 24 | class InputQueue: 25 | def __init__(self, config, scheduler, statistics): 26 | self.config = config 27 | self.scheduler = scheduler 28 | self.bitmap_storage = BitmapStorage(config, config.config_values['BITMAP_SHM_SIZE'], "master", read_only=False) 29 | self.id_to_node = {} 30 | self.current_cycle = [] 31 | self.bitmap_index_to_fav_node = {} 32 | self.num_cycles = 0 33 | self.pending_favorites = True 34 | self.statistics = statistics 35 | self.grimoire_scheduler = GrimoireScheduler() 36 | 37 | def get_next(self): 38 | assert self.id_to_node 39 | 40 | gram = self.grimoire_scheduler.get_next() 41 | if gram: 42 | return gram 43 | 44 | node = self.current_cycle.pop() if self.current_cycle else None 45 | while node: 46 | if self.scheduler.should_be_scheduled(self, node): 47 | return node 48 | node = self.current_cycle.pop() if self.current_cycle else None 49 | self.update_current_cycle() 50 | return self.get_next() 51 | 52 | def has_inputs(self): 53 | return len(self.id_to_node) > 0 54 | 55 | def update_current_cycle(self): 56 | self.num_cycles += 1 57 | self.current_cycle = list(self.id_to_node.values()) 58 | self.cull_queue(self.current_cycle) 59 | self.sort_queue(self.current_cycle) 60 | self.statistics.event_queue_cycle(self) 61 | 62 | def cull_queue(self, nodes): 63 | self.pending_favorites = False 64 | for (index, (node, _)) in self.bitmap_index_to_fav_node.iteritems(): 65 | if node.get_state() != "finished": 66 | self.pending_favorites = True 67 | # TODO implement queue culling like afl? 68 | 69 | def sort_queue(self, nodes): 70 | nodes.sort(key=lambda n: self.scheduler.score_priority(n)) 71 | 72 | def get_node_by_id(self, id): 73 | return self.id_to_node[id] 74 | 75 | def num_inputs(self): 76 | len(self.id_to_node) 77 | 78 | def construct_node(self, payload, bitmap, new_bytes, new_bits, node_struct, ): 79 | assert "fav_bits" not in node_struct 80 | assert "level" not in node_struct 81 | assert "new_bytes" not in node_struct 82 | assert "new_bits" not in node_struct 83 | node_struct["new_bytes"] = new_bytes 84 | node_struct["new_bits"] = new_bits 85 | 86 | node = QueueNode(payload, bitmap, node_struct, write=False) 87 | node.clear_fav_bits(write=False) 88 | parent = node_struct["info"]["parent"] 89 | node.set_level(self.get_node_by_id(parent).get_level() + 1 if parent else 0, write=False) 90 | return node 91 | 92 | def maybe_insert_node(self, payload, bitmap_array, node_struct): 93 | bitmap = ExecutionResult.bitmap_from_bytearray(bitmap_array, node_struct["info"]["exit_reason"], 94 | node_struct["info"]["performance"]) 95 | bitmap.lut_applied = True # since we received the bitmap from the slave, the lut was already applied 96 | backup_data = bitmap.copy_to_array() 97 | should_store, new_bytes, new_bits = self.bitmap_storage.should_store_in_queue(bitmap) 98 | new_data = bitmap.copy_to_array() 99 | if should_store: 100 | self.insert_input(self.construct_node(payload, bitmap_array, new_bytes, new_bits, node_struct), bitmap) 101 | else: 102 | for i in xrange(len(bitmap_array)): 103 | if backup_data[i] != new_data[i]: 104 | print("diffing at {} {} {}".format(i, repr(backup_data[i]), repr(new_data[i]))) 105 | safe_print("RECIEVED BORING INPUT, NOT SAVING..") 106 | # assert(False) 107 | 108 | def insert_input(self, node, bitmap): 109 | safe_print(repr(node.node_struct)) 110 | self.grimoire_scheduler.insert_input(node) 111 | node.set_fav_factor(self.scheduler.score_fav(node), write=True) 112 | self.id_to_node[node.get_id()] = node 113 | self.current_cycle.append(node) 114 | print("saving input") 115 | self.update_best_input_for_bitmap_entry(node, bitmap) # TODO improve performance! 116 | print("done saving input") 117 | self.sort_queue(self.current_cycle) 118 | 119 | self.statistics.event_new_node_found(node) 120 | 121 | def should_overwrite_old_entry(self, index, val, node): 122 | entry = self.bitmap_index_to_fav_node.get(index) 123 | if not entry: 124 | return True, None 125 | old_node, old_val = entry 126 | more_bits = val > old_val 127 | better_score = (val == old_val and node.get_fav_factor() < old_node.get_fav_factor()) 128 | if more_bits or better_score: 129 | return True, old_node 130 | return False, None 131 | 132 | def update_best_input_for_bitmap_entry(self, node, bitmap): 133 | changed_nodes = set() 134 | for (index, val) in enumerate(bitmap.cbuffer): 135 | if val == 0x0: 136 | continue 137 | overwrite, old_node = self.should_overwrite_old_entry(index, val, node) 138 | if overwrite: 139 | self.bitmap_index_to_fav_node[index] = (node, val) 140 | node.add_fav_bit(index, write=False) 141 | changed_nodes.add(node) 142 | if old_node: 143 | old_node.remove_fav_bit(index, write=False) 144 | changed_nodes.add(old_node) 145 | for node in changed_nodes: 146 | node.write_metadata() 147 | -------------------------------------------------------------------------------- /fuzzer/scheduler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | from fuzzer.technique.helper import RAND 18 | 19 | 20 | class GrimoireScheduler: 21 | def __init__(self): 22 | self.nodes = [] 23 | 24 | def get_next(self): 25 | if len(self.nodes) > 0: 26 | return self.nodes.pop(0) 27 | return None 28 | 29 | def insert_input(self, node): 30 | if len(node.get_new_bytes()) > 0: 31 | node.set_state("grimoire_inference") 32 | self.nodes.append(node) 33 | 34 | 35 | class Scheduler: 36 | 37 | def __init__(self): 38 | pass 39 | 40 | def should_be_scheduled(self, queue, node): 41 | SKIP_TO_NEW_PROB = 99 # ...when there are new, pending favorites 42 | SKIP_NFAV_OLD_PROB = 95 # ...no new favs, cur entry already fuzzed 43 | SKIP_NFAV_NEW_PROB = 75 # ...no new favs, cur entry not fuzzed yet 44 | 45 | if node.get_exit_reason() != "regular": 46 | return False 47 | 48 | if queue.pending_favorites: 49 | if (node.get_state() == "finished" or not node.get_favorite()) and RAND(100) < SKIP_TO_NEW_PROB: 50 | return False 51 | elif not node.get_favorite() and queue.num_inputs() > 10: 52 | if queue.num_cycles >= 1 and node.get_state() != "finished": 53 | if RAND(100) < SKIP_NFAV_NEW_PROB: 54 | return False 55 | else: 56 | if RAND(100) < SKIP_NFAV_OLD_PROB: 57 | return False 58 | return True 59 | 60 | def score_priority(self, node): 61 | if node.get_performance() == 0: 62 | return (0,) 63 | 64 | is_fast = 1 if 1 / node.get_performance() >= 150 else 0 # TODO calculate adaptive as fastest n% or similar metric for "fast" 65 | 66 | return (is_fast, len(node.get_fav_bits()), -node.get_level(), -node.get_fav_factor()) 67 | 68 | def score_fav(self, node): 69 | return node.get_performance() * node.get_payload_len() 70 | 71 | def get_attention(node): 72 | return 1 73 | -------------------------------------------------------------------------------- /fuzzer/statistics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import msgpack 18 | import time 19 | 20 | from common.util import atomic_write 21 | 22 | 23 | class MasterStatistics: 24 | def __init__(self, config): 25 | self.config = config 26 | self.data = { 27 | "yield": {}, 28 | "queue": { 29 | "cycles": 0, 30 | "states": {} 31 | }, 32 | "bytes_in_bitmap": 0, 33 | } 34 | self.filename = self.config.argument_values['work_dir'] + "/stats" 35 | 36 | def event_queue_cycle(self, queue): 37 | data_states = self.data["queue"]["states"] = {} 38 | self.data["queue"]["cycles"] += 1 39 | for id in queue.id_to_node: 40 | node = queue.id_to_node[id] 41 | state = node.get_state() 42 | data_states = self.data["queue"]["states"] 43 | if state not in data_states: 44 | data_states[state] = 0 45 | data_states[state] += 1 46 | self.write_statistics() 47 | 48 | def event_new_node_found(self, node): 49 | self.update_bitmap_bytes(node) 50 | self.update_yield(node) 51 | self.update_inputs(node) 52 | 53 | self.write_statistics() 54 | 55 | def update_inputs(self, node): 56 | exitreason = node.get_exit_reason() 57 | if not exitreason in self.data["queue"]: 58 | self.data["queue"][exitreason] = { 59 | "num": 0, 60 | "last_found": None 61 | } 62 | info = self.data["queue"][exitreason] 63 | info["num"] += 1 64 | info["last_found"] = time.ctime(node.node_struct["info"]["time"]) 65 | 66 | def update_bitmap_bytes(self, node): 67 | self.data["bytes_in_bitmap"] += len(node.node_struct["new_bytes"]) 68 | 69 | def update_yield(self, node): 70 | method = node.node_struct["info"]["method"] 71 | if method not in self.data["yield"]: 72 | self.data["yield"][method] = 0 73 | self.data["yield"][method] += 1 74 | 75 | def write_statistics(self): 76 | atomic_write(self.filename, msgpack.packb(self.data)) 77 | 78 | 79 | class SlaveStatistics: 80 | def __init__(self, slave_id, config): 81 | self.config = config 82 | self.filename = self.config.argument_values['work_dir'] + "/slave_stats_%d" % (slave_id) 83 | self.data = { 84 | "start_time": time.time(), 85 | "executions": 0, 86 | "executions_redqueen": 0, 87 | "node_id": None, 88 | } 89 | 90 | def event_task(self, task): 91 | self.data["node_id"] = task.get("nid", None) 92 | self.write_statistics() 93 | 94 | def event_exec(self): 95 | self.data["executions"] += 1 96 | if self.data["executions"] % 1000 == 0: 97 | self.write_statistics() 98 | 99 | def event_exec_redqueen(self): 100 | self.data["executions_redqueen"] += 1 101 | 102 | def write_statistics(self): 103 | self.data["duration"] = time.time() - self.data["start_time"] 104 | self.data["execs/sec"] = (self.data["executions"] + self.data["executions_redqueen"]) / self.data["duration"] 105 | atomic_write(self.filename, msgpack.packb(self.data)) 106 | -------------------------------------------------------------------------------- /fuzzer/technique/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sergej' 2 | -------------------------------------------------------------------------------- /fuzzer/technique/arithmetic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import ctypes 18 | from array import array 19 | 20 | from fuzzer.technique.helper import * 21 | 22 | __author__ = 'sergej' 23 | 24 | 25 | def arithmetic_range(data, skip_null=False, effector_map=None, set_arith_max=None): 26 | if len(data) == 0: 27 | return 0 28 | 29 | if not set_arith_max: 30 | set_arith_max = AFL_ARITH_MAX 31 | 32 | data_len = len(data) 33 | num = 0 34 | 35 | if effector_map: 36 | byte_count = 0 37 | for i in range(len(data)): 38 | if effector_map[i]: 39 | byte_count += 1 40 | num += (set_arith_max * 2) 41 | if byte_count >= 2: 42 | num += ((set_arith_max - 2) * 4) 43 | if byte_count >= 4: 44 | num += ((set_arith_max - 2) * 4) 45 | 46 | else: 47 | byte_count = 0 48 | else: 49 | num += (data_len * (set_arith_max * 2)) 50 | 51 | if data_len > 1: 52 | num += ((data_len - 1) * ((set_arith_max - 2) * 4)) 53 | if data_len > 2: 54 | num += ((data_len - 3) * ((set_arith_max - 2) * 4)) 55 | 56 | return num 57 | 58 | 59 | def mutate_seq_8_bit_arithmetic_array(data, func, default_info, skip_null=False, effector_map=None, set_arith_max=None): 60 | if not set_arith_max: 61 | set_arith_max = AFL_ARITH_MAX 62 | 63 | for i in range(0, len(data)): 64 | if effector_map: 65 | if not effector_map[i]: 66 | continue 67 | if skip_null and data[i] == 0x00: 68 | continue 69 | 70 | for j in range(1, set_arith_max + 1): 71 | r = data[i] ^ (data[i] + j) 72 | if is_not_bitflip(ctypes.c_uint8(r).value): 73 | data[i] = (data[i] + j) & 0xff 74 | func(data.tostring(), default_info) 75 | data[i] = (data[i] - j) & 0xff 76 | 77 | r = data[i] ^ (data[i] - j) 78 | if is_not_bitflip(ctypes.c_uint8(r).value): 79 | data[i] = (data[i] - j) & 0xff 80 | func(data.tostring(), default_info) 81 | data[i] = (data[i] + j) & 0xff 82 | 83 | 84 | def mutate_seq_16_bit_arithmetic_array(data, func, default_info, skip_null=False, effector_map=None, 85 | set_arith_max=None): 86 | if not set_arith_max: 87 | set_arith_max = AFL_ARITH_MAX 88 | 89 | for i in range(0, len(data) - 1): 90 | # log_master("orig: %s"%repr(data[i:i+2])) 91 | # log_master("string: %s"%repr(data[i:i+2].tostring())) 92 | value = array('H', (data[i:i + 2]).tostring()) 93 | # log_master("array: %s"%repr(value)) 94 | value = in_range_16(value[0]) 95 | # log_master("in range: %s"%repr(value)) 96 | if effector_map: 97 | if not (effector_map[i] or effector_map[i + 1]): 98 | continue 99 | if skip_null and value == 0x00: 100 | continue 101 | for j in range(1, set_arith_max + 1): 102 | 103 | # log_master("perform arith 16 on %d %d value: %x +j %x -j %x"%(i,j,value, value+j, value-j)) 104 | r1 = (value ^ in_range_16(value + j)) 105 | r2 = (value ^ in_range_16(value - j)) 106 | r3 = value ^ swap_16(swap_16(value) + j) 107 | r4 = value ^ swap_16(swap_16(value) - j) 108 | 109 | # little endian increment 110 | if is_not_bitflip(r1) and ((value & 0xff) + j) > 0xff: 111 | # log_master("perform little endian %d +%d = %d"%(i,j, in_range_16(value + j))); 112 | func(data[:i].tostring() + to_string_16(in_range_16(value + j)) + data[i + 2:].tostring(), default_info) 113 | 114 | # little endian decrement 115 | if is_not_bitflip(r2) and (value & 0xff) < j: 116 | # log_master("perform little endian %d -%d = %d"%(i,j, in_range_16(value - j))); 117 | func(data[:i].tostring() + to_string_16(in_range_16(value - j)) + data[i + 2:].tostring(), default_info) 118 | 119 | # if swap_16(in_range_16(value + j)) == in_range_16(value + j) or swap_16(in_range_16(value - j)) == in_range_16(value - j): 120 | # continue 121 | 122 | # big endian increment 123 | if is_not_bitflip(r3) and ((value >> 8) + j) > 0xff: 124 | # log_master("perform big endian %d +%d = %d"%(i,j, swap_16(in_range_16(swap_16(value) + j)))); 125 | func(data[:i].tostring() + to_string_16(swap_16(in_range_16(swap_16(value) + j))) + data[ 126 | i + 2:].tostring(), 127 | default_info) 128 | 129 | # big endian decrement 130 | if is_not_bitflip(r4) and (value >> 8) < j: 131 | # log_master("perform big endian %d -%d = %d"%(i,j, swap_16(in_range_16(swap_16(value) - j)))); 132 | func(data[:i].tostring() + to_string_16(swap_16(in_range_16(swap_16(value) - j))) + data[ 133 | i + 2:].tostring(), 134 | default_info) 135 | 136 | 137 | def mutate_seq_32_bit_arithmetic_array(data, func, default_info, skip_null=False, effector_map=None, 138 | set_arith_max=None): 139 | if not set_arith_max: 140 | set_arith_max = AFL_ARITH_MAX 141 | 142 | for i in range(0, len(data) - 3): 143 | value = array('I', (data[i:i + 4]).tostring()) 144 | value = in_range_32(value[0]) 145 | 146 | if effector_map: 147 | if not (effector_map[i] or effector_map[i + 1] or effector_map[i + 2] or effector_map[i + 3]): 148 | # log_master("eff skip %d"%i); 149 | continue 150 | 151 | if skip_null and value == 0x00: 152 | continue 153 | for j in range(1, set_arith_max + 1): 154 | 155 | r1 = (value ^ in_range_32(value + j)) 156 | r2 = (value ^ in_range_32(value - j)) 157 | r3 = value ^ swap_32(swap_32(value) + j) 158 | r4 = value ^ swap_32(swap_32(value) - j) 159 | 160 | # log_master("perform arith 32 on %d %d rs: %d %d %d %d"%(i,j, r1,r2,r3,r4)); 161 | # little endian increment 162 | if is_not_bitflip(r1) and in_range_32((value & 0xffff) + j) > 0xffff: 163 | # log_master("perform little endian %d -%d = %d"%(i,j, in_range_32(value + j))); 164 | func(data[:i].tostring() + to_string_32(in_range_32(value + j)) + data[i + 4:].tostring(), default_info) 165 | 166 | # little endian decrement 167 | if is_not_bitflip(r2) and in_range_32(value & 0xffff) < j: 168 | # log_master("perform little endian %d -%d = %d"%(i,j, in_range_32(value - j))); 169 | func(data[:i].tostring() + to_string_32(in_range_32(value - j)) + data[i + 4:].tostring(), default_info) 170 | 171 | # if swap_32(in_range_32(value + j)) == in_range_32(value + j) or swap_32(in_range_32(value - j)) == in_range_32(value - j): 172 | # continue 173 | 174 | # big endian increment 175 | if is_not_bitflip(r3) and in_range_32((swap_32(value) & 0xffff) + j) > 0xffff: 176 | # log_master("perform big endian %d +%d = %d"%(i,j, swap_32(in_range_32(swap_32(value) + j)))); 177 | func(data[:i].tostring() + to_string_32(swap_32(in_range_32(swap_32(value) + j))) + data[ 178 | i + 4:].tostring(), 179 | default_info) 180 | 181 | # big endian decrement 182 | if is_not_bitflip(r4) and (swap_32(value) & 0xffff) < j: 183 | # log_master("perform big endian %d -%d = %d"%(i,j, swap_32(in_range_32(swap_32(value) - j)))); 184 | func(data[:i].tostring() + to_string_32(swap_32(in_range_32(swap_32(value) - j))) + data[ 185 | i + 4:].tostring(), 186 | default_info) 187 | -------------------------------------------------------------------------------- /fuzzer/technique/bitflip.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | 19 | def bitflip_range(data, skip_null=False, effector_map=None): 20 | if len(data) == 0: 21 | return 0 22 | 23 | if effector_map: 24 | effector_map = effector_map[:len(data)] 25 | data_len = sum(x is True for x in effector_map) 26 | data_tmp = "" 27 | for i in range(len(data)): 28 | if effector_map[i]: 29 | data_tmp += data[i] 30 | else: 31 | data_len = len(data) 32 | data_tmp = data 33 | num = data_len * 8 34 | num += data_len * 7 35 | num += data_len * 5 36 | num += data_len 37 | if effector_map: 38 | byte_count = 0 39 | for i in range(len(data)): 40 | if effector_map[i]: 41 | byte_count += 1 42 | if byte_count >= 2: 43 | num += 1 44 | if byte_count >= 4: 45 | num += 1 46 | 47 | else: 48 | byte_count = 0 49 | else: 50 | if data_len > 1: 51 | num += data_len - 1 52 | if data_len > 3: 53 | num += data_len - 3 54 | return num 55 | 56 | 57 | def bitflip8_range(data, skip_null=False, effector_map=None): 58 | if effector_map: 59 | effector_map = effector_map[:len(data)] 60 | data_len = sum(x is True for x in effector_map) 61 | data_tmp = "" 62 | for i in range(len(data)): 63 | if effector_map[i]: 64 | data_tmp += data[i] 65 | else: 66 | data_len = len(data) 67 | data_tmp = data 68 | num = data_len * 8 69 | return num 70 | 71 | 72 | def mutate_seq_walking_bits_array(data, func, default_info, skip_null=False, effector_map=None): 73 | for i in xrange(len(data) * 8): 74 | if effector_map: 75 | if not effector_map[i / 8]: 76 | continue 77 | if data[i / 8] == 0x00 and skip_null: 78 | func(None, no_data=True) 79 | continue 80 | data[i / 8] ^= (0x80 >> (i % 8)) 81 | func(data.tostring(), default_info) 82 | data[i / 8] ^= (0x80 >> (i % 8)) 83 | 84 | 85 | def mutate_seq_two_walking_bits_array(data, func, default_info, skip_null=False, effector_map=None): 86 | for i in range((len(data) * 8) - 1): 87 | if effector_map: 88 | if not (effector_map[i / 8] or effector_map[(i + 1) / 8]): 89 | continue 90 | if data[i / 8] == 0x00 and data[(i + 1) / 8] == 0x00 and skip_null: 91 | func(None, no_data=True) 92 | continue 93 | data[i / 8] ^= (0x80 >> (i % 8)) 94 | data[(i + 1) / 8] ^= (0x80 >> ((i + 1) % 8)) 95 | func(data.tostring(), default_info) 96 | data[i / 8] ^= (0x80 >> (i % 8)) 97 | data[(i + 1) / 8] ^= (0x80 >> ((i + 1) % 8)) 98 | 99 | 100 | def mutate_seq_four_walking_bits_array(data, func, default_info, skip_null=False, effector_map=None): 101 | for i in range((len(data) * 8 - 3)): 102 | if effector_map: 103 | if not (effector_map[i / 8] or effector_map[(i + 3) / 8]): 104 | continue 105 | if data[i / 8] == 0x00 and data[(i + 3) / 8] == 0x00 and skip_null: 106 | func(None, no_data=True) 107 | continue 108 | 109 | data[i / 8] ^= (0x80 >> (i % 8)) 110 | data[(i + 1) / 8] ^= (0x80 >> ((i + 1) % 8)) 111 | data[(i + 2) / 8] ^= (0x80 >> ((i + 2) % 8)) 112 | data[(i + 3) / 8] ^= (0x80 >> ((i + 3) % 8)) 113 | func(data.tostring(), default_info) 114 | data[i / 8] ^= (0x80 >> (i % 8)) 115 | data[(i + 1) / 8] ^= (0x80 >> ((i + 1) % 8)) 116 | data[(i + 2) / 8] ^= (0x80 >> ((i + 2) % 8)) 117 | data[(i + 3) / 8] ^= (0x80 >> ((i + 3) % 8)) 118 | 119 | 120 | def mutate_seq_walking_byte_array(data, func, default_info, effector_map, limiter_map, skip_null=False): 121 | orig_bitmap, _ = func(data.tostring(), default_info) 122 | # mmh3.hash64(bitmap) 123 | for i in range((len(data))): 124 | if limiter_map: 125 | if not limiter_map[i]: 126 | continue 127 | if data[i] == 0x00 and skip_null: 128 | continue 129 | data[i] ^= 0xFF 130 | bitmap, _ = func(data.tostring(), default_info) 131 | if effector_map and orig_bitmap == bitmap: 132 | effector_map[i] = 0 133 | data[i] ^= 0xFF 134 | 135 | 136 | def mutate_seq_two_walking_bytes_array(data, func, default_info, effector_map=None): 137 | if len(data) > 1: 138 | for i in range(1, ((len(data)))): 139 | if effector_map: 140 | if not (effector_map[i] or effector_map[i - 1]): 141 | continue 142 | data[i] ^= 0xFF 143 | data[i - 1] ^= 0xFF 144 | func(data.tostring(), default_info) 145 | data[i] ^= 0xFF 146 | data[i - 1] ^= 0xFF 147 | 148 | 149 | def mutate_seq_four_walking_bytes_array(data, func, default_info, effector_map=None): 150 | if len(data) > 3: 151 | for i in range(3, (len(data))): 152 | if effector_map: 153 | if not (effector_map[i] or effector_map[i - 1] or effector_map[i - 2] or effector_map[i - 3]): 154 | continue 155 | data[i - 0] ^= 0xFF 156 | data[i - 1] ^= 0xFF 157 | data[i - 2] ^= 0xFF 158 | data[i - 3] ^= 0xFF 159 | func(data.tostring(), default_info) 160 | data[i - 0] ^= 0xFF 161 | data[i - 1] ^= 0xFF 162 | data[i - 2] ^= 0xFF 163 | data[i - 3] ^= 0xFF 164 | -------------------------------------------------------------------------------- /fuzzer/technique/debug.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | 19 | def mutate_seq_debug_array(data, func, skip_null=False, kafl_state=None): 20 | kafl_state["technique"] = "DEBUG" 21 | for i in range(len(data) * 0xff): 22 | # tmp = data[i/0xff] 23 | # data[i/0xff] = (i % 0xff) 24 | func(data.tostring()) 25 | # data[i/0xff] = tmp 26 | -------------------------------------------------------------------------------- /fuzzer/technique/grimoire_inference.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import re 18 | from collections import OrderedDict 19 | 20 | from common.debug import log_grimoire 21 | 22 | 23 | class GrimoireInference: 24 | 25 | def __init__(self, config, verify_input): 26 | self.config = config 27 | self.verify_input = verify_input 28 | self.generalized_inputs = OrderedDict({tuple(["gap"]): 0}) 29 | self.tokens = OrderedDict({tuple([""]): 0}) 30 | self.strings = [] 31 | self.strings_regex = None 32 | self.load_strings() 33 | 34 | @staticmethod 35 | def wordlist_to_regex(words): 36 | escaped = map(re.escape, words) 37 | combined = '|'.join(sorted(escaped, key=len, reverse=True)) 38 | return re.compile(combined) 39 | 40 | def load_strings(self): 41 | if not self.config.argument_values["I"]: 42 | return 43 | 44 | path = self.config.argument_values["I"] 45 | strings = [] 46 | 47 | for l in open(path): 48 | if not l.startswith("#"): 49 | try: 50 | s = (l.split("=\"")[1].split("\"\n")[0]).decode("string_escape") 51 | if s == "": 52 | continue 53 | self.tokens[tuple([c for c in s])] = 0 54 | strings.append(s) 55 | except: 56 | pass 57 | self.strings = strings 58 | self.strings_regex = self.wordlist_to_regex(strings) 59 | 60 | @staticmethod 61 | def char_class_to_str(name): 62 | if name == "gap": 63 | return "" 64 | else: 65 | return name 66 | 67 | def generalized_to_string(self, generalized_input): 68 | return "".join([self.char_class_to_str(char_class) for char_class in generalized_input]) 69 | 70 | @staticmethod 71 | def to_printable_char_class(name): 72 | if name == "gap": 73 | return "{}" 74 | else: 75 | return name 76 | 77 | @staticmethod 78 | def trim_generalized(generalized_input): 79 | ret = [] 80 | before = "" 81 | for char_class in generalized_input: 82 | if char_class == before and char_class == "gap": 83 | pass 84 | else: 85 | ret.append(char_class) 86 | before = char_class 87 | return ret 88 | 89 | def find_gaps(self, payload, old_node, default_info, find_next_index, split_char): 90 | index = 0 91 | while index < len(payload): 92 | resume_index = find_next_index(payload, index, split_char) 93 | test_payload = self.generalized_to_string(payload[0:index] + payload[resume_index:]) 94 | 95 | if self.verify_input(test_payload, old_node, default_info): 96 | res = "gap" 97 | payload[index:resume_index] = [res] * (resume_index - index) 98 | 99 | index = resume_index 100 | 101 | return self.trim_generalized(payload) 102 | 103 | def find_gaps_in_closures(self, payload, old_node, default_info, find_closures, opening_char, closing_char): 104 | 105 | index = 0 106 | while index < len(payload): 107 | index, endings = find_closures(payload, index, opening_char, closing_char) 108 | 109 | if len(endings) == 0: 110 | return payload 111 | 112 | ending = len(payload) 113 | while endings: 114 | ending = endings.pop(0) 115 | 116 | test_payload = self.generalized_to_string(payload[0:index] + payload[ending:]) 117 | 118 | if self.verify_input(test_payload, old_node, default_info): 119 | res = "gap" 120 | 121 | payload[index:ending] = [res] * (ending - index) 122 | 123 | break 124 | 125 | index = ending 126 | 127 | return self.trim_generalized(payload) 128 | 129 | def generalize_input(self, payload, old_node, default_info): 130 | if not self.verify_input(payload, old_node, default_info): 131 | return None, None 132 | 133 | log_grimoire("generalizing input {} with bytes {}".format(repr(payload), old_node["new_bytes"])) 134 | generalized_input = [c for c in payload] 135 | 136 | def increment_by_offset(_, index, offset): 137 | return index + offset 138 | 139 | def find_next_char(l, index, char): 140 | while index < len(l): 141 | if l[index] == char: 142 | return index + 1 143 | 144 | index += 1 145 | 146 | return index 147 | 148 | def find_closures(l, index, opening_char, closing_char): 149 | endings = [] 150 | 151 | while index < len(l): 152 | if l[index] == opening_char: 153 | break 154 | index += 1 155 | 156 | start_index = index 157 | index_ending = len(l) - 1 158 | 159 | while index_ending > start_index: 160 | if l[index_ending] == closing_char: 161 | endings.append(index_ending + 1) 162 | index_ending -= 1 163 | 164 | index += 1 165 | return start_index, endings 166 | 167 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, increment_by_offset, 256) 168 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, increment_by_offset, 128) 169 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, increment_by_offset, 64) 170 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, increment_by_offset, 32) 171 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, increment_by_offset, 1) 172 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, ".", ) 173 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, ";") 174 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, ",") 175 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, "\n") 176 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, "\r") 177 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, "#") 178 | generalized_input = self.find_gaps(generalized_input, old_node, default_info, find_next_char, " ") 179 | 180 | generalized_input = self.find_gaps_in_closures(generalized_input, old_node, default_info, find_closures, "(", 181 | ")") 182 | generalized_input = self.find_gaps_in_closures(generalized_input, old_node, default_info, find_closures, "[", 183 | "]") 184 | generalized_input = self.find_gaps_in_closures(generalized_input, old_node, default_info, find_closures, "{", 185 | "}") 186 | generalized_input = self.find_gaps_in_closures(generalized_input, old_node, default_info, find_closures, "<", 187 | ">") 188 | generalized_input = self.find_gaps_in_closures(generalized_input, old_node, default_info, find_closures, "'", 189 | "'") 190 | generalized_input = self.find_gaps_in_closures(generalized_input, old_node, default_info, find_closures, "\"", 191 | "\"") 192 | 193 | generalized_input = self.finalize_generalized(generalized_input) 194 | 195 | if len(generalized_input) > 8192: 196 | return None, None 197 | 198 | self.add_to_inputs(generalized_input) 199 | printable_generalized = self.to_printable(generalized_input) 200 | log_grimoire("final class learnt: {}".format(repr(printable_generalized))) 201 | log_grimoire("new input: {}".format(repr(self.generalized_to_string(generalized_input)))) 202 | 203 | return repr(printable_generalized), generalized_input 204 | 205 | def to_printable(self, generalized_input): 206 | return "".join([self.to_printable_char_class(char_class) for char_class in generalized_input]) 207 | 208 | @staticmethod 209 | def finalize_generalized(generalized_input): 210 | return tuple(generalized_input) 211 | 212 | @staticmethod 213 | def tokenize(generalized_input): 214 | token = [] 215 | for char_class in generalized_input: 216 | if char_class != "gap": 217 | token.append(char_class) 218 | else: 219 | if token: 220 | yield tuple(token) 221 | token = [] 222 | yield tuple(token) 223 | 224 | def add_to_inputs(self, generalized_input): 225 | assert isinstance(generalized_input, tuple) 226 | 227 | if generalized_input not in self.generalized_inputs: 228 | self.generalized_inputs[generalized_input] = 0 229 | self.generalized_inputs[generalized_input] += 1 230 | 231 | if self.generalized_inputs[generalized_input] > 1: 232 | return 233 | 234 | for token in self.tokenize(generalized_input): 235 | if len(token) < 2: 236 | continue 237 | if token not in self.tokens: 238 | log_grimoire("adding token {}".format(repr(token))) 239 | self.tokens[token] = 0 240 | self.tokens[token] += 1 241 | -------------------------------------------------------------------------------- /fuzzer/technique/grimoire_mutations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | from common.debug import log_grimoire 18 | from fuzzer.technique.helper import RAND 19 | 20 | CHOOSE_SUBINPUT = 50 21 | RECURSIVE_REPLACEMENT_DEPTH = [2, 4, 8, 16, 32, 64] 22 | 23 | 24 | def filter_gap_indices(generalized_input): 25 | return [index for index in xrange(len(generalized_input)) if generalized_input[index] == "gap"] 26 | 27 | 28 | def find_string_matches(generalized_input, grimoire_inference): 29 | if grimoire_inference.strings_regex == None: 30 | return [] 31 | payload = grimoire_inference.generalized_to_string(generalized_input) 32 | string_matches = [match for match in grimoire_inference.strings_regex.finditer(payload)] 33 | 34 | log_grimoire("{} string matches for {} strings".format(len(string_matches), len(grimoire_inference.strings))) 35 | 36 | return string_matches 37 | 38 | 39 | def pad_generalized_input(generalized_input): 40 | if not generalized_input: 41 | return tuple(["gap"]) 42 | if not generalized_input[0] == "gap": 43 | generalized_input = tuple(["gap"]) + generalized_input 44 | if not generalized_input[-1] == "gap": 45 | generalized_input = generalized_input + tuple(["gap"]) 46 | return generalized_input 47 | 48 | 49 | def random_generalized(grimoire_inference): 50 | rand_generalized = grimoire_inference.generalized_inputs.keys()[RAND(len(grimoire_inference.generalized_inputs))] 51 | rand_generalized = pad_generalized_input(rand_generalized) 52 | 53 | if RAND(100) > CHOOSE_SUBINPUT and len(rand_generalized) > 0: 54 | if RAND(100) < 50 and len(rand_generalized) > 0: 55 | gap_indices = filter_gap_indices(rand_generalized) 56 | min_index, max_index = gap_indices[RAND(len(gap_indices))], gap_indices[ 57 | RAND(len(gap_indices))] 58 | min_index, max_index = min(min_index, max_index), max(min_index, max_index) 59 | rand_generalized = rand_generalized[min_index:max_index + 1] 60 | else: 61 | random_token = grimoire_inference.tokens.keys()[RAND(len(grimoire_inference.tokens))] 62 | rand_generalized = pad_generalized_input(random_token) 63 | 64 | assert rand_generalized[0] == "gap" and rand_generalized[-1] == "gap" 65 | return rand_generalized 66 | 67 | 68 | def recursive_replacement(generalized_input, grimoire_inference, depth): 69 | for _ in xrange(depth): 70 | 71 | if len(generalized_input) >= 64 << 10: 72 | return generalized_input 73 | 74 | gap_indices = filter_gap_indices(generalized_input) 75 | 76 | if len(gap_indices) == 0: 77 | return generalized_input 78 | 79 | random_index = gap_indices[RAND(len(gap_indices))] 80 | 81 | generalized_input = generalized_input[0:random_index] + random_generalized( 82 | grimoire_inference) + generalized_input[random_index + 1:] 83 | 84 | return generalized_input 85 | 86 | 87 | def mutate_recursive_replacement(generalized_input, func, default_info, grimoire_inference): 88 | default_info["method"] = "grimoire_recursive_replacement" 89 | 90 | depth = RECURSIVE_REPLACEMENT_DEPTH[RAND(len(RECURSIVE_REPLACEMENT_DEPTH))] 91 | generalized_input = recursive_replacement(generalized_input, grimoire_inference, depth) 92 | data = grimoire_inference.generalized_to_string(generalized_input) 93 | 94 | func(data, default_info) 95 | 96 | 97 | def mutate_input_extension(generalized_input, func, default_info, grimoire_inference): 98 | default_info["method"] = "grimoire_input_extension" 99 | 100 | rand_generalized = random_generalized(grimoire_inference) 101 | 102 | data = grimoire_inference.generalized_to_string(rand_generalized) + grimoire_inference.generalized_to_string( 103 | generalized_input) 104 | func(data, default_info) 105 | 106 | data = grimoire_inference.generalized_to_string(generalized_input) + grimoire_inference.generalized_to_string( 107 | rand_generalized) 108 | func(data, default_info) 109 | 110 | 111 | def mutate_replace_strings(generalized_input, func, default_info, grimoire_inference, string_matches): 112 | if len(string_matches) == 0: 113 | return 114 | 115 | default_info["method"] = "grimoire_replace_strings" 116 | 117 | payload = grimoire_inference.generalized_to_string(generalized_input) 118 | 119 | match = string_matches[RAND((len(string_matches)))] 120 | rand_str = grimoire_inference.strings[RAND(len(grimoire_inference.strings))] 121 | 122 | # replace single instance 123 | data = payload[0:match.start()] + rand_str + payload[match.end():] 124 | func(data, default_info) 125 | 126 | # replace all instances 127 | data = payload.replace(payload[match.start():match.end()], rand_str) 128 | func(data, default_info) 129 | 130 | 131 | def havoc(generalized_input, func, default_info, grimoire_inference, max_iterations, generalized): 132 | generalized_input = pad_generalized_input(generalized_input) 133 | assert generalized_input[0] == "gap" and generalized_input[-1] == "gap" 134 | 135 | string_matches = find_string_matches(generalized_input, grimoire_inference) 136 | 137 | for _ in xrange(max_iterations): 138 | if generalized: 139 | mutate_input_extension(generalized_input, func, default_info, grimoire_inference) 140 | mutate_recursive_replacement(generalized_input, func, default_info, grimoire_inference) 141 | mutate_replace_strings(generalized_input, func, default_info, grimoire_inference, string_matches) 142 | -------------------------------------------------------------------------------- /fuzzer/technique/havoc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import glob 18 | 19 | from common.config import FuzzerConfiguration 20 | from fuzzer.technique.havoc_handler import * 21 | 22 | 23 | def load_dict(file_name): 24 | f = open(file_name) 25 | dict_entries = [] 26 | for line in f: 27 | if not line.startswith("#"): 28 | try: 29 | dict_entries.append((line.split("=\"")[1].split("\"\n")[0]).decode("string_escape")) 30 | except: 31 | pass 32 | f.close() 33 | return dict_entries 34 | 35 | 36 | def init_havoc(config): 37 | global location_corpus 38 | if config.argument_values["I"]: 39 | set_dict(load_dict(FuzzerConfiguration().argument_values["I"])) 40 | append_handler(havoc_dict) 41 | append_handler(havoc_dict) 42 | 43 | location_corpus = config.argument_values['work_dir'] + "/corpus/" 44 | 45 | 46 | def havoc_range(perf_score): 47 | max_iterations = int(perf_score * 2.5) 48 | 49 | if max_iterations < AFL_HAVOC_MIN: 50 | max_iterations = AFL_HAVOC_MIN 51 | 52 | return max_iterations 53 | 54 | 55 | def mutate_seq_havoc_array(data, func, default_info, max_iterations, stacked=True, resize=False, files_to_splice=None): 56 | reseed() 57 | if resize: 58 | copy = array('B', data.tostring() + data.tostring()) 59 | else: 60 | copy = array('B', data.tostring()) 61 | 62 | cnt = 0 63 | for i in range(max_iterations): 64 | # if resize: 65 | # copy = array('B', data.tostring() + data.tostring()) 66 | # else: 67 | copy = array('B', data.tostring()) 68 | 69 | value = RAND(AFL_HAVOC_STACK_POW2) 70 | 71 | for j in range(1 << (1 + value)): 72 | handler = havoc_handler[RAND(len(havoc_handler))] 73 | # if not stacked: 74 | # if resize: 75 | # copy = array('B', data.tostring() + data.tostring()) 76 | # else: 77 | # copy = array('B', data.tostring()) 78 | copy = handler(copy) 79 | if len(copy) >= 64 << 10: 80 | copy = copy[:(64 << 10)] 81 | # cnt += 1 82 | # if cnt >= max_iterations: 83 | # return 84 | func(copy.tostring(), default_info) 85 | pass 86 | 87 | 88 | def mutate_seq_splice_array(data, func, default_info, max_iterations, stacked=True, resize=False): 89 | files = glob.glob(location_corpus + "/*/payload_*") 90 | random.shuffle(files) 91 | mutate_seq_havoc_array(havoc_splicing(data, files), func, default_info, max_iterations, stacked=stacked, 92 | resize=resize) 93 | -------------------------------------------------------------------------------- /fuzzer/technique/havoc_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | from array import array 18 | 19 | from common.debug import log_redq 20 | from common.util import read_binary_file, find_diffs 21 | from fuzzer.technique.helper import * 22 | 23 | 24 | def insert_word(data, charset, start, term): 25 | if len(data) >= 2: 26 | offset = RAND(len(data)) 27 | if RAND(2) > 1: 28 | repllen = 0 # plain insert 29 | else: 30 | replen = RAND(len(data) - offset) 31 | 32 | word_length = min(len(data) - offset, RAND(10) + 1) 33 | 34 | head = data[:offset].tostring() 35 | tail = data[offset + replen:].tostring() 36 | 37 | body = "".join([charset[RAND(len(charset))] for _ in xrange(word_length - 1)]) 38 | inputstr = head + term + body + term + tail 39 | return array("B", inputstr) 40 | return data 41 | 42 | 43 | def havoc_insert_line(data): 44 | alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy" 45 | num = "0123456789.,x" 46 | special = "!\"$%&/()=?`'#+*+-_,.;:\\{[]}<>" 47 | charsets = [alpha, num, special] 48 | terminator = ["\n", " ", "\0", '""', "'", "", " ADF\n"] 49 | start_term = terminator[RAND(len(terminator))] 50 | end_term = terminator[RAND(len(terminator))] 51 | return insert_word(data, charsets[RAND(len(charsets))], start_term, end_term) 52 | 53 | 54 | def havoc_perform_bit_flip(data): 55 | if len(data) >= 1: 56 | bit = RAND(len(data) << 3) 57 | data[bit / 8] ^= 0x80 >> (bit % 8) 58 | return data 59 | 60 | 61 | def havoc_perform_insert_interesting_value_8(data): 62 | if len(data) >= 1: 63 | offset = RAND(len(data)) 64 | value_index = RAND(len(interesting_8_Bit)) 65 | data[offset] = in_range_8(interesting_8_Bit[value_index]) 66 | return data 67 | 68 | 69 | def havoc_perform_insert_interesting_value_16(data): 70 | if len(data) >= 2: 71 | little = RAND(2) 72 | pos = RAND(len(data) - 1) 73 | value_index = RAND(len(interesting_16_Bit)) 74 | interesting_value = in_range_16(interesting_16_Bit[value_index]) 75 | if little == 0: 76 | interesting_value = swap_16(interesting_value) 77 | store_16(data, pos, interesting_value) 78 | return data 79 | 80 | 81 | def havoc_perform_insert_interesting_value_32(data): 82 | if len(data) >= 4: 83 | 84 | little = RAND(2) 85 | pos = RAND(len(data) - 3) 86 | interesting_value = in_range_32(interesting_32_Bit[RAND(len(interesting_32_Bit))]) 87 | if little == 0: 88 | interesting_value = swap_32(interesting_value) 89 | store_32(data, pos, interesting_value) 90 | return data 91 | 92 | 93 | def havoc_perform_byte_subtraction_8(data): 94 | if len(data) >= 1: 95 | delta = RAND(AFL_ARITH_MAX) 96 | pos = RAND(len(data)) 97 | value = load_8(data, pos) 98 | value -= 1 + delta 99 | store_8(data, pos, value) 100 | return data 101 | 102 | 103 | def havoc_perform_byte_addition_8(data): 104 | if len(data) >= 1: 105 | delta = RAND(AFL_ARITH_MAX) 106 | pos = RAND(len(data)) 107 | value = load_8(data, pos) 108 | value += 1 + delta 109 | store_8(data, pos, value) 110 | return data 111 | 112 | 113 | def havoc_perform_byte_subtraction_16(data): 114 | if len(data) >= 2: 115 | little = RAND(2) 116 | pos = RAND(len(data) - 1) 117 | value = load_16(data, pos) 118 | if little == 0: 119 | value = swap_16(swap_16(value) - (1 + RAND(AFL_ARITH_MAX))) 120 | else: 121 | value -= 1 + RAND(AFL_ARITH_MAX) 122 | store_16(data, pos, value) 123 | return data 124 | 125 | 126 | def havoc_perform_byte_addition_16(data): 127 | if len(data) >= 2: 128 | little = RAND(2) 129 | pos = RAND(len(data) - 1) 130 | value = load_16(data, pos) 131 | if little == 0: 132 | value = swap_16(swap_16(value) + (1 + RAND(AFL_ARITH_MAX))) 133 | else: 134 | value += 1 + RAND(AFL_ARITH_MAX) 135 | store_16(data, pos, value) 136 | return data 137 | 138 | 139 | def havoc_perform_byte_subtraction_32(data): 140 | if len(data) >= 4: 141 | little = RAND(2) 142 | pos = RAND(len(data) - 3) 143 | value = load_32(data, pos) 144 | if little == 0: 145 | value = swap_32(swap_32(value) - (1 + RAND(AFL_ARITH_MAX))) 146 | else: 147 | value -= 1 + RAND(AFL_ARITH_MAX) 148 | store_32(data, pos, value) 149 | return data 150 | 151 | 152 | def havoc_perform_byte_addition_32(data): 153 | if len(data) >= 4: 154 | little = RAND(2) 155 | pos = RAND(len(data) - 3) 156 | value = load_32(data, pos) 157 | if little == 0: 158 | value = swap_32(swap_32(value) + (1 + RAND(AFL_ARITH_MAX))) 159 | else: 160 | value += 1 + RAND(AFL_ARITH_MAX) 161 | store_32(data, pos, value) 162 | return data 163 | 164 | 165 | def havoc_perform_set_random_byte_value(data): 166 | if len(data) >= 1: 167 | delta = 1 + RAND(0xff) 168 | data[RAND(len(data))] ^= delta 169 | return data 170 | 171 | 172 | # Todo: somehow broken :-( 173 | def havoc_perform_delete_random_byte(data): 174 | if len(data) >= 2: 175 | del_length = AFL_choose_block_len(len(data) - 1) 176 | del_from = RAND(len(data) - del_length + 1) 177 | data = data[:del_from] + data[del_from + del_length:] 178 | return data 179 | 180 | 181 | def havoc_perform_clone_random_byte(data): 182 | temp_len = len(data) 183 | if len(data) > 2: 184 | if (temp_len + HAVOC_BLK_LARGE) < KAFL_MAX_FILE: 185 | actually_clone = RAND(4); 186 | if actually_clone != 0: 187 | clone_len = AFL_choose_block_len(temp_len); 188 | clone_from = RAND(temp_len - clone_len + 1); 189 | else: 190 | clone_len = AFL_choose_block_len(HAVOC_BLK_XL); 191 | clone_from = 0; 192 | 193 | clone_to = RAND(temp_len); 194 | 195 | head = data[:clone_to].tostring() 196 | 197 | if actually_clone != 0: 198 | body = data[clone_from: clone_from + clone_len].tostring() 199 | else: 200 | if RAND(2) != 0: 201 | val = chr(RAND(256)) 202 | else: 203 | val = chr(data[RAND(temp_len)]) 204 | body = ''.join(val for _ in range(clone_len)) 205 | 206 | tail = data[clone_to:].tostring() 207 | data = array('B', head + body + tail) 208 | return data 209 | 210 | 211 | def havoc_perform_byte_seq_override(data): 212 | if len(data) >= 2: 213 | copy_length = AFL_choose_block_len(len(data) - 1) 214 | copy_from = RAND(len(data) - copy_length + 1) 215 | copy_to = RAND(len(data) - copy_length + 1) 216 | if RAND(4) != 0: 217 | if copy_from != copy_to: 218 | chunk = data[copy_from: copy_from + copy_length] 219 | for i in range(len(chunk)): 220 | data[i + copy_to] = chunk[i] 221 | else: 222 | if RAND(2) == 1: 223 | value = RAND(256) 224 | else: 225 | value = data[RAND(len(data))] 226 | for i in range(copy_length): 227 | data[i + copy_to] = value 228 | return data 229 | 230 | 231 | def havoc_perform_byte_seq_extra1(data): 232 | pass 233 | 234 | 235 | def havoc_perform_byte_seq_extra2(data): 236 | pass 237 | 238 | 239 | def havoc_splicing(data, files=None): 240 | if len(data) >= 2: 241 | for file in files: 242 | file_data = read_binary_file(file) 243 | if len(file_data) < 2: 244 | continue 245 | 246 | first_diff, last_diff = find_diffs(data, file_data) 247 | if last_diff < 2 or first_diff == last_diff: 248 | continue 249 | 250 | split_location = first_diff + RAND(last_diff - first_diff) 251 | 252 | data = array('B', data.tostring()[:split_location] + file_data[split_location:]) 253 | # func(data.tostring()) 254 | break 255 | 256 | return data 257 | 258 | 259 | dict_set = set() 260 | dict_import = [] 261 | 262 | redqueen_dict = {} 263 | redqueen_addr_list = [] 264 | redqueen_known_addrs = set() 265 | redqueen_seen_addr_to_value = {} 266 | 267 | 268 | def set_dict(new_dict): 269 | global dict_import 270 | dict_import = new_dict 271 | dict_set = set(new_dict) 272 | 273 | 274 | def clear_redqueen_dict(): 275 | global redqueen_dict, redqueen_addr_list 276 | log_redq("clearing dict %s" % repr(redqueen_dict)) 277 | redqueen_dict = {} 278 | redqueen_addr_list = [] 279 | 280 | 281 | def get_redqueen_dict(): 282 | global redqueen_dict 283 | return redqueen_dict 284 | 285 | 286 | def get_redqueen_seen_addr_to_value(): 287 | global redqueen_seen_addr_to_value 288 | return redqueen_seen_addr_to_value 289 | 290 | 291 | def add_to_redqueen_dict(addr, val): 292 | global redqueen_dict, redqueen_addr_list 293 | 294 | assert (len(redqueen_dict) == len(redqueen_addr_list)) 295 | 296 | val = val[:16] 297 | for v in val.split("\0"): 298 | if len(v) > 3: 299 | if not addr in redqueen_dict: 300 | redqueen_dict[addr] = set() 301 | redqueen_addr_list.append(addr) 302 | # log_redq("Added Dynamic Dict: %s"%repr(v)) 303 | redqueen_dict[addr].add(v) 304 | 305 | 306 | def append_handler(handler): 307 | global havoc_handler 308 | havoc_handler.append(handler) 309 | 310 | 311 | def apply_dict_to_data(data, entry, entry_pos): 312 | newdata = array('B', data.tostring()[:entry_pos] + entry + data.tostring()[entry_pos + len(entry):]) 313 | # log_redq("HAVOC DICT: %s [%s] -> %s "%(repr(data.tostring()),repr(entry), repr(newdata.tostring()))) 314 | return newdata 315 | 316 | 317 | def havoc_dict(data): 318 | global redqueen_dict 319 | global dict_import 320 | 321 | has_redq = len(redqueen_dict) > 0 322 | has_dict = len(dict_import) > 0 323 | coin = RAND(2) != 0 324 | 325 | if has_redq and ((not has_dict) or coin): 326 | addr = redqueen_addr_list[RAND(len(redqueen_addr_list))] 327 | dict_values = list(redqueen_dict[addr]) 328 | dict_entry = dict_values[RAND(len(dict_values))] 329 | entry_pos = RAND(max([0, len(data) - len(dict_entry)])) 330 | return apply_dict_to_data(data, dict_entry, entry_pos) 331 | 332 | if has_dict: 333 | dict_entry = dict_import[RAND(len(dict_import))] 334 | dict_entry = dict_entry[:len(data)] 335 | entry_pos = RAND(max([0, len(data) - len(dict_entry)])) 336 | return apply_dict_to_data(data, dict_entry, entry_pos) 337 | return data 338 | 339 | 340 | havoc_handler = [havoc_perform_bit_flip, 341 | havoc_perform_insert_interesting_value_8, 342 | havoc_perform_insert_interesting_value_16, 343 | havoc_perform_insert_interesting_value_32, 344 | havoc_perform_byte_subtraction_8, 345 | havoc_perform_byte_addition_8, 346 | havoc_perform_byte_subtraction_16, 347 | havoc_perform_byte_addition_16, 348 | havoc_perform_byte_subtraction_32, 349 | havoc_perform_byte_addition_32, 350 | havoc_perform_set_random_byte_value, 351 | havoc_perform_delete_random_byte, 352 | havoc_perform_delete_random_byte, 353 | havoc_perform_clone_random_byte, 354 | havoc_perform_byte_seq_override, 355 | havoc_dict 356 | 357 | # havoc_perform_clone_random_byte, 358 | # havoc_perform_byte_seq_override, 359 | # havoc_perform_byte_seq_extra1, 360 | # havoc_perform_byte_seq_extra2, 361 | # havoc_insert_line, 362 | ] 363 | -------------------------------------------------------------------------------- /fuzzer/technique/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import ctypes 18 | import fastrand 19 | import inspect 20 | import os 21 | import random 22 | import struct 23 | from ctypes import * 24 | 25 | KAFL_MAX_FILE = 1 << 15 26 | 27 | HAVOC_BLK_SMALL = 32 28 | HAVOC_BLK_MEDIUM = 128 29 | HAVOC_BLK_LARGE = 1500 30 | HAVOC_BLK_XL = 32768 31 | 32 | AFL_ARITH_MAX = 35 33 | AFL_HAVOC_MIN = 500 34 | AFL_HAVOC_CYCLES = 5000 35 | AFL_HAVOC_STACK_POW2 = 7 36 | 37 | interesting_8_Bit = [-128, -1, 0, 1, 16, 32, 64, 100, 127] 38 | interesting_16_Bit = interesting_8_Bit + [-32768, -129, 128, 255, 256, 512, 1000, 1024, 4096, 32767] 39 | interesting_32_Bit = interesting_16_Bit + [-2147483648, -100663046, -32769, 32768, 65535, 65536, 100663045, 2147483647] 40 | 41 | random.seed(os.urandom(4)) 42 | 43 | 44 | def random_string(): 45 | baselen = 4 << RAND(8) 46 | strlen = (RAND(3) + 1) * baselen + RAND(1) * RAND(baselen) 47 | return "".join([chr(RAND(256)) for x in xrange(strlen)]) 48 | 49 | 50 | # Todo 51 | def AFL_choose_block_len(limit): 52 | global HAVOC_BLK_SMALL 53 | global HAVOC_BLK_MEDIUM 54 | global HAVOC_BLK_LARGE 55 | global HAVOC_BLK_XL 56 | min_value = 1 57 | max_value = 32 58 | 59 | # u32 rlim = MIN(queue_cycle, 3); 60 | # if (!run_over10m) rlim = 1; 61 | rlim = 1 62 | case = RAND(rlim) 63 | if case == 0: 64 | min_value = 1 65 | max_value = HAVOC_BLK_SMALL 66 | elif case == 1: 67 | min_value = HAVOC_BLK_SMALL 68 | max_value = HAVOC_BLK_MEDIUM 69 | else: 70 | case = RAND(10) 71 | if case == 0: 72 | min_value = HAVOC_BLK_LARGE 73 | max_value = HAVOC_BLK_XL 74 | else: 75 | min_value = HAVOC_BLK_MEDIUM 76 | max_value = HAVOC_BLK_LARGE 77 | 78 | if min_value >= limit: 79 | min_value = 1; 80 | 81 | return min_value + RAND(MIN(max_value, limit) - min_value + 1); 82 | 83 | 84 | # Todo 85 | def AFL_choose_block_len2(limit): 86 | min_value = 1 87 | max_value = 16 88 | 89 | if min_value >= limit: 90 | min_value = limit 91 | 92 | return min_value + RAND(MIN(max_value, limit) - min_value + 1) 93 | 94 | 95 | def MIN(value_a, value_b): 96 | if value_a > value_b: 97 | return value_b 98 | else: 99 | return value_a 100 | 101 | 102 | def reseed(): 103 | random.seed(os.urandom(4)) 104 | 105 | 106 | def RAND(value): 107 | if value == 0: 108 | return value 109 | return fastrand.pcg32bounded(value) 110 | 111 | 112 | def load_8(value, pos): 113 | return value[pos] 114 | 115 | 116 | def load_16(value, pos): 117 | return (value[pos + 1] << 8) + value[pos + 0] 118 | 119 | 120 | def load_32(value, pos): 121 | return (value[pos + 3] << 24) + (value[pos + 2] << 16) + (value[pos + 1] << 8) + value[pos + 0] 122 | 123 | 124 | def store_8(data, pos, value): 125 | data[pos] = in_range_8(value) 126 | 127 | 128 | def store_16(data, pos, value): 129 | value = in_range_16(value) 130 | data[pos + 1] = (value & 0xff00) >> 8 131 | data[pos] = (value & 0x00ff) 132 | 133 | 134 | def store_32(data, pos, value): 135 | value = in_range_32(value) 136 | data[pos + 3] = (value & 0xff000000) >> 24 137 | data[pos + 2] = (value & 0x00ff0000) >> 16 138 | data[pos + 1] = (value & 0x0000ff00) >> 8 139 | data[pos + 0] = (value & 0x000000ff) 140 | 141 | 142 | def in_range_8(value): 143 | return ctypes.c_uint8(value).value 144 | 145 | 146 | def in_range_16(value): 147 | return ctypes.c_uint16(value).value 148 | 149 | 150 | def in_range_32(value): 151 | return ctypes.c_uint32(value).value 152 | 153 | 154 | def swap_16(value): 155 | res = in_range_16((((value & 0xff00) >> 8) + ((value & 0xff) << 8))) 156 | return res 157 | 158 | 159 | def swap_32(value): 160 | return ((value & 0x000000ff) << 24) + \ 161 | ((value & 0x0000ff00) << 8) + \ 162 | ((value & 0x00ff0000) >> 8) + \ 163 | ((value & 0xff000000) >> 24) 164 | 165 | 166 | def to_string_8(value): 167 | res1 = struct.pack(". 16 | """ 17 | 18 | from fuzzer.technique.helper import * 19 | 20 | __author__ = 'sergej' 21 | 22 | 23 | def interesting_range(data, skip_null=False, effector_map=None): 24 | if effector_map: 25 | data_len = sum(x is True for x in effector_map) 26 | data_tmp = "" 27 | for i in range(len(data)): 28 | if effector_map[i]: 29 | data_tmp += data[i] 30 | else: 31 | data_len = len(data) 32 | data_tmp = data 33 | if skip_null: 34 | num_of_non_null_bytes = data_len - data_tmp.count('\x00') 35 | num = num_of_non_null_bytes * len(interesting_8_Bit) 36 | num += ((num_of_non_null_bytes - 1) * (len(interesting_16_Bit))) * 2 37 | num += ((num_of_non_null_bytes - 3) * (len(interesting_32_Bit))) * 2 38 | else: 39 | num = data_len * len(interesting_8_Bit) 40 | if data_len > 1: 41 | num += ((data_len - 1) * (len(interesting_16_Bit))) * 2 42 | if data_len > 3: 43 | num += ((data_len - 3) * (len(interesting_32_Bit))) * 2 44 | if num < 0: 45 | return 0 46 | 47 | return num 48 | 49 | 50 | def mutate_seq_8_bit_interesting_array(data, func, default_info, skip_null=False, effector_map=None): 51 | for i in range(0, len(data)): 52 | if effector_map: 53 | if not effector_map[i]: 54 | continue 55 | byte = data[i] 56 | for j in range(len(interesting_8_Bit)): 57 | if skip_null and byte == 0: 58 | continue 59 | interesting_value = in_range_8(interesting_8_Bit[j]) 60 | if is_not_bitflip(byte ^ interesting_value) and is_not_arithmetic(byte, interesting_value, 1): 61 | func(data[:i].tostring() + to_string_8(interesting_value) + data[(i + 1):].tostring(), default_info) 62 | 63 | data[i] = byte 64 | 65 | 66 | def mutate_seq_16_bit_interesting_array(data, func, default_info, skip_null=False, effector_map=None, 67 | set_arith_max=None): 68 | for i in range(len(data) - 1): 69 | if effector_map: 70 | if not (effector_map[i] or effector_map[i + 1]): 71 | continue 72 | 73 | byte1 = data[i + 1] 74 | byte2 = data[i] 75 | value = (byte1 << 8) + byte2 76 | 77 | if skip_null and value == 0: 78 | continue 79 | 80 | for j in range(len(interesting_16_Bit)): 81 | interesting_value = in_range_16(interesting_16_Bit[j]) 82 | swapped_value = swap_16(interesting_value) 83 | 84 | if is_not_bitflip(value ^ interesting_value) and \ 85 | is_not_arithmetic(value, interesting_value, 2, set_arith_max=set_arith_max) and \ 86 | is_not_interesting(value, interesting_value, 2, 0): 87 | func(data[:i].tostring() + to_string_16(interesting_value) + data[(i + 2):].tostring(), default_info) 88 | 89 | if interesting_value != swapped_value and \ 90 | is_not_bitflip(value ^ swapped_value) and \ 91 | is_not_arithmetic(value, swapped_value, 2, set_arith_max=set_arith_max) and \ 92 | is_not_interesting(value, swapped_value, 2, 1): 93 | func(data[:i].tostring() + to_string_16(swapped_value) + data[(i + 2):].tostring(), default_info) 94 | 95 | 96 | def mutate_seq_32_bit_interesting_array(data, func, default_info, skip_null=False, effector_map=None, 97 | set_arith_max=None): 98 | for i in range(len(data) - 3): 99 | if effector_map: 100 | if not (effector_map[i] or effector_map[i + 1] or effector_map[i + 2] or effector_map[i + 3]): 101 | continue 102 | 103 | byte1 = data[i + 3] 104 | byte2 = data[i + 2] 105 | byte3 = data[i + 1] 106 | byte4 = data[i + 0] 107 | value = (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4 108 | 109 | if skip_null and value == 0: 110 | continue 111 | 112 | for j in range(len(interesting_32_Bit)): 113 | interesting_value = in_range_32(interesting_32_Bit[j]) 114 | swapped_value = swap_32(interesting_value) 115 | if is_not_bitflip(value ^ interesting_value) and \ 116 | is_not_arithmetic(value, interesting_value, 4, set_arith_max=set_arith_max) and \ 117 | is_not_interesting(value, interesting_value, 4, 0): 118 | func(data[:i].tostring() + to_string_32(interesting_value) + data[(i + 4):].tostring(), default_info) 119 | 120 | if interesting_value != swapped_value and \ 121 | is_not_bitflip(value ^ swapped_value) and \ 122 | is_not_arithmetic(value, swapped_value, 4, set_arith_max=set_arith_max) and \ 123 | is_not_interesting(value, swapped_value, 4, 1): 124 | func(data[:i].tostring() + to_string_32(swapped_value) + data[(i + 4):].tostring(), default_info) 125 | -------------------------------------------------------------------------------- /fuzzer/technique/radamsa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RUB-SysSec/grimoire/fae90ee436fe626f54ec2421269de8340cf06bf9/fuzzer/technique/radamsa -------------------------------------------------------------------------------- /fuzzer/technique/radamsa.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import glob 18 | import os 19 | import random 20 | import socket 21 | import subprocess 22 | import time 23 | 24 | from common.config import FuzzerConfiguration 25 | from common.debug import logger 26 | 27 | 28 | def radamsa_range(perf_score): 29 | max_iterations = int(perf_score * 2.5) 30 | 31 | if max_iterations < AFL_HAVOC_MIN: 32 | max_iterations = AFL_HAVOC_MIN 33 | 34 | return max_iterations 35 | 36 | 37 | def execute(cmd): 38 | logger("CMD: " + cmd) 39 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None, shell=True) 40 | return proc 41 | 42 | 43 | # proc.wait() 44 | 45 | location_corpus = FuzzerConfiguration().argument_values['work_dir'] + "/corpus/" 46 | 47 | 48 | def mutate_seq_radamsa_array(data, func, default_info, max_iterations): 49 | logger("FILES: " + str(len(os.listdir(location_corpus)))) 50 | default_info["method"] = "radamsa" 51 | files = sorted(glob.glob(location_corpus + "/*/payload_*")) 52 | last_n = 5 53 | rand_n = 5 54 | samples = files[-last_n:] + random.sample(files[:-last_n], max(0, min(rand_n, len(files) - last_n))) 55 | try: 56 | if samples: 57 | proc = execute("./fuzzer/technique/radamsa -o :21337 -n inf " + " ".join(samples)) 58 | 59 | while True: 60 | # logger("TRY") 61 | try: 62 | s = socket.create_connection(("127.0.0.1", 21337), timeout=1) 63 | s.recv(1) 64 | break 65 | except Exception as e: 66 | logger("In Radamsa: " + str(e)) 67 | time.sleep(0.1) 68 | finally: 69 | try: 70 | s.close() 71 | except: 72 | pass 73 | 74 | # logger("DONE") 75 | for i in range(max_iterations): 76 | s = socket.create_connection(("127.0.0.1", 21337)) 77 | payload = s.recv(65530) 78 | s.close() 79 | size = len(payload) 80 | # logger("SIZE is: " + str(size)) 81 | 82 | if size > (64 << 10): 83 | payload = payload[:(2 << 10)] 84 | if size == 0: 85 | func(data.tostring(), default_info) 86 | else: 87 | # func(data.tostring()) 88 | func(payload[:(2 << 10)], default_info) 89 | 90 | proc.kill() 91 | proc.wait() 92 | except: 93 | pass 94 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/cmp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import itertools 18 | import re 19 | import struct 20 | 21 | import fuzzer 22 | from common.debug import log_redq 23 | from encoding import Encoders 24 | from fuzzer.technique import havoc_handler 25 | 26 | MAX_NUMBER_PERMUTATIONS = 256 # number of trials per address, lhs and encoding 27 | 28 | HAMMER_LEA = False 29 | known_lea_offsets = set() 30 | 31 | 32 | def enable_hammering(): 33 | global HAMMER_LEA 34 | log_redq("Hammering enabled!") 35 | HAMMER_LEA = True 36 | 37 | 38 | class Cmp: 39 | def __init__(self, addr, type, size, is_imm): 40 | self.addr = addr 41 | self.type = type 42 | self.size = size 43 | self.is_imm = is_imm 44 | 45 | self.run_info_to_pairs = {} 46 | self.enc_and_val_to_encval = {} 47 | self.run_infos_with_not_all_found = {} 48 | self.original_rhs = set() 49 | self.original_lhs = set() 50 | self.colored_rhs = set() 51 | self.colored_lhs = set() 52 | self.num_mutations = None 53 | self.hammer = (not self.addr in known_lea_offsets) and (self.type in ["LEA", "SUB", "ADD"]) 54 | known_lea_offsets.add(self.addr) 55 | self.offsets_and_lhs_to_rhs = {} 56 | index = None 57 | 58 | def add_result(self, run_info, lhs, rhs): 59 | self.run_info_to_pairs[run_info] = self.run_info_to_pairs.get(run_info, set()) 60 | if run_info.was_colored: 61 | self.colored_lhs.add(lhs) 62 | self.colored_rhs.add(rhs) 63 | self.run_info_to_pairs[run_info].add((lhs, rhs)) 64 | else: 65 | if not self.is_simple_mutation(lhs, rhs): 66 | self.original_lhs.add(lhs) 67 | self.original_rhs.add(rhs) 68 | self.run_info_to_pairs[run_info].add((lhs, rhs)) 69 | 70 | def is_simple_mutation(self, lhs, rhs): 71 | if lhs == rhs: 72 | return True 73 | if self.type == "STR": 74 | return False 75 | else: 76 | unpack_keys = {1: "B", 2: "H", 4: "L", 8: "Q"} 77 | bytes = self.size / 8 78 | key = unpack_keys.get(bytes, None) 79 | ilhs = struct.unpack(">" + key, lhs)[0] 80 | irhs = struct.unpack(">" + key, rhs)[0] 81 | if abs(ilhs - irhs) < fuzzer.technique.helper.AFL_ARITH_MAX: 82 | return True 83 | if lhs == "\0" * bytes: 84 | return True 85 | return False 86 | 87 | def was_true_in(self, run_info): 88 | # print self.run_info_to_results[run_info] 89 | # log_redq("check if cmp was satisfied: %s"%repr(self.run_info_to_pairs[run_info])) 90 | return all([lhs == rhs for (lhs, rhs) in self.run_info_to_pairs[run_info]]) 91 | 92 | def __calc_available_encoders(self): 93 | for enc in Encoders: 94 | if all([self.__is_valid_encoder_for(enc, run_info) for run_info in self.run_info_to_pairs]): 95 | yield (enc) 96 | 97 | def __is_valid_encoder_for(self, enc, run_info): 98 | for (lhs, rhs) in self.run_info_to_pairs[run_info]: 99 | if not enc.is_applicable(self, lhs, rhs): 100 | return False 101 | return True 102 | 103 | def calc_mutations(self, orig_run_info, num_runs): 104 | self.num_mutations = 0 105 | for enc in self.__calc_available_encoders(): 106 | cmp_encoded = CmpEncoded(self, enc) 107 | if cmp_encoded.is_interessting(orig_run_info, num_runs): 108 | for (offsets, lhs, rhs) in cmp_encoded.get_mutations(orig_run_info): 109 | self.num_mutations += 1 110 | yield (offsets, lhs, rhs, enc) 111 | 112 | def could_be_hash(self): 113 | # log_redq("Got cmp @ %x could be hash?"%self.addr) 114 | # log_redq("orig_lhs \t%s"%repr(self.original_lhs)) 115 | # log_redq("colo_lhs\t%s"%repr(self.colored_lhs)) 116 | 117 | # log_redq("orig_rhs \t%s"%repr(self.original_rhs)) 118 | # log_redq("colo_rhs\t%s"%repr(self.colored_rhs)) 119 | # assert(self.num_mutations != None) 120 | if not self.num_mutations or self.num_mutations > 16: 121 | return False 122 | if self.is_imm or self.type != "CMP" or not self.size > 8: 123 | return False 124 | if len(self.original_lhs) > 32: 125 | return False 126 | if self.original_lhs == self.colored_lhs and self.original_rhs == self.colored_rhs: 127 | return False 128 | if all([lhs.count("\0") > 0 for lhs in self.original_lhs]) and all( 129 | [lhs.count("\0") > 0 for lhs in self.original_lhs]): 130 | return False 131 | # log_redq("Got cmp @ %x could be hash?"%self.addr) 132 | # log_redq("orig_lhs \t%s"%repr(self.original_lhs)) 133 | # log_redq("colo_lhs\t%s"%repr(self.colored_lhs)) 134 | 135 | # log_redq("orig_rhs \t%s"%repr(self.original_rhs)) 136 | # log_redq("colo_rhs\t%s"%repr(self.colored_rhs)) 137 | return True 138 | 139 | 140 | class CmpEncoded: 141 | def __init__(self, cmp, encoding): 142 | self.cmp = cmp 143 | self.enc = encoding 144 | self.occured_in_all_run_infos = True 145 | self.mutations = None 146 | self.all_valid_offsets = None 147 | self.val_to_encval = {} 148 | 149 | def __get_encoded(self, val): 150 | if (val) in self.val_to_encval: 151 | return self.val_to_encval[val] 152 | encval = self.enc.encode(self.cmp, val) 153 | self.val_to_encval[val] = encval 154 | return encval 155 | 156 | def __restrict_offset_tuple(self, offset_tuple, orig_run_info): 157 | res = list(offset_tuple) 158 | valid_offsets = self.get_offset_intersect_tuple(orig_run_info) 159 | if valid_offsets != None: 160 | for i in xrange(len(offset_tuple)): 161 | res[i] &= valid_offsets[i] 162 | else: 163 | assert (len(self.cmp.run_info_to_pairs) == 1) 164 | return res 165 | 166 | def get_offset_intersect_tuple(self, orig_info): 167 | if self.all_valid_offsets: 168 | return self.all_valid_offsets 169 | for run_info in self.cmp.run_info_to_pairs: 170 | if run_info != orig_info: 171 | if not self.all_valid_offsets: 172 | self.all_valid_offsets = self.get_offset_union_tuple(run_info) 173 | else: 174 | other_offsets = self.get_offset_union_tuple(run_info) 175 | for i in xrange(len(self.all_valid_offsets)): 176 | self.all_valid_offsets[i] &= other_offets[i] 177 | return self.all_valid_offsets 178 | 179 | def get_offset_union_tuple(self, run_info): 180 | union_tuple = [set() for _ in xrange(self.enc.size())] 181 | set_of_lhs = set() 182 | for (lhs, rhs) in self.cmp.run_info_to_pairs[run_info]: 183 | set_of_lhs.add(lhs) 184 | for lhs in set_of_lhs: 185 | offset_tuple = run_info.get_offset_tuple(self.__get_encoded(lhs)) 186 | for i in xrange(len(offset_tuple)): 187 | union_tuple[i] |= offset_tuple[i] 188 | 189 | if not all(union_tuple): 190 | self.occured_in_all_run_infos = False 191 | 192 | return union_tuple 193 | 194 | def get_str_variants(self, rhs): 195 | base = self.__get_encoded(rhs)[0] 196 | res = [(base,)] 197 | if not "\0" in base: 198 | res += [(base + "\0",)] 199 | if re.match('^[[:print:]]+$', base): 200 | if not "\n" in base: 201 | res += [(base + "\n",)] 202 | if not " " in base: 203 | res += [(base + " ",)] 204 | if not '"' in base: 205 | res += [('"' + base + '"',)] 206 | if not "'" in base: 207 | res += [("'" + base + "'",)] 208 | return res 209 | # return [ (base+"\n",), (base+"\0",), (base,)] 210 | # return [ (base+"\n",), (base+"\0",), (base+" ",), ('"'+base+'"',),("'"+base+"'",), (base,)] 211 | 212 | def get_int_variants(self, rhs): 213 | global HAMMER_LEA 214 | res = [tuple(self.__get_encoded(rhs))] 215 | unpack_keys = {1: "B", 2: "H", 4: "L", 8: "Q"} 216 | bytes = self.cmp.size / 8 217 | key = unpack_keys.get(bytes, None) 218 | max = 2 ** (8 * bytes) - 1 219 | val = struct.unpack(">" + key, rhs)[0] 220 | max_offset = 1 221 | if HAMMER_LEA and self.cmp.hammer: 222 | max_offset = 64 223 | for i in xrange(1, max_offset + 1): 224 | res.append(tuple(self.__get_encoded(struct.pack(">" + key, (val + i) % max)))) 225 | res.append(tuple(self.__get_encoded(struct.pack(">" + key, (val - i) % max)))) 226 | return tuple(res) 227 | 228 | def get_sub_variants(self, rhs): 229 | res = [] 230 | unpack_keys = {1: "B", 2: "H", 4: "L", 8: "Q"} 231 | bytes = self.cmp.size / 8 232 | key = unpack_keys.get(bytes, None) 233 | max = 2 ** (8 * bytes) - 1 234 | val = struct.unpack(">" + key, rhs)[0] 235 | for i in xrange(-16, 16): 236 | res.append(tuple(self.__get_encoded(struct.pack(">" + key, (val + i) % max)))) 237 | return tuple(res) 238 | 239 | def get_variants(self, rhs): 240 | if self.cmp.type == "STR": 241 | return self.get_str_variants(rhs) 242 | elif self.cmp.type == "SUB": 243 | return self.get_sub_variants(rhs) 244 | else: 245 | return self.get_int_variants(rhs) 246 | 247 | def register_dict(self, repl): 248 | # log_redq("add to dict: %s"%repr(repl)) 249 | if len(repl) > 2: 250 | havoc_handler.add_to_redqueen_dict(self.cmp.addr, repl) 251 | 252 | def get_mutations(self, orig_run_info): 253 | if self.mutations != None: 254 | return self.mutations 255 | self.mutations = set() 256 | for (lhs, rhs) in self.cmp.run_info_to_pairs[orig_run_info]: 257 | if self.enc.is_redundant(self.cmp, lhs, rhs): 258 | continue 259 | pattern_tuple = tuple(self.__get_encoded(lhs)) 260 | offsets_tuple = orig_run_info.get_offset_tuple(pattern_tuple) 261 | 262 | offsets_tuple = self.__restrict_offset_tuple(offsets_tuple, orig_run_info) 263 | if not all(offsets_tuple): 264 | continue 265 | self.register_dict(rhs) 266 | for offset_tuple in itertools.islice(itertools.product(*offsets_tuple), MAX_NUMBER_PERMUTATIONS): 267 | for repl_tuple in self.get_variants(rhs): 268 | if pattern_tuple != repl_tuple: 269 | self.mutations.add((offset_tuple, pattern_tuple, repl_tuple)) 270 | return self.mutations 271 | 272 | def is_interessting(self, orig_run_info, num_runs): 273 | mutations = self.get_mutations(orig_run_info) 274 | if len(mutations) > 0 and len(mutations) < 256: 275 | return True 276 | return self.occured_in_all_run_infos 277 | 278 | def could_be_hash(self): 279 | always_found = len(self.run_infos_where_not_all_found) == 0 280 | return always_found and self.cmp._could_be_hash() 281 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/colorize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | # import time 18 | import array 19 | import random 20 | 21 | 22 | # definition of range indicies: 23 | # array = [a,b,c,d] 24 | # the range (0,0) contains no element 25 | # the range (0,1) contains exactly the element a 26 | # the amount of elements in a range is max_-min_ 27 | 28 | class ColorizerStrategy: 29 | COLORABLE = 1 30 | UNKNOWN = 0 31 | FIXED = -1 32 | 33 | def __init__(self, data_length, checker): 34 | self.color_info = array.array('b', [self.UNKNOWN for _ in xrange(0, data_length)]) 35 | self.unknown_ranges = set() 36 | if data_length > 0: 37 | self.add_unknown_range(0, data_length) 38 | self.checker = checker 39 | 40 | def is_range_colorable(self, min_, max_): 41 | if self.checker(min_, max_): 42 | for i in xrange(min_, max_): 43 | self.color_info[i] = self.COLORABLE 44 | return True 45 | else: 46 | if min_ + 1 == max_: 47 | self.color_info[min_] = self.FIXED 48 | return False 49 | 50 | def bin_search(self, min_, max_): 51 | if self.is_range_colorable(min_, max_) or min_ + 1 == max_: 52 | return 53 | center = min_ + (max_ - min_) / 2 54 | self.add_unknown_range(min_, center) 55 | self.add_unknown_range(center, max_) 56 | 57 | def colorize_step(self): 58 | (min_i, max_i) = max(self.unknown_ranges, key=lambda (mi, ma): ma - mi) 59 | self.unknown_ranges.remove((min_i, max_i)) 60 | self.bin_search(min_i, max_i) 61 | 62 | def add_unknown_range(self, min_, max_): 63 | assert (min_ < max_) 64 | self.unknown_ranges.add((min_, max_)) 65 | 66 | 67 | import unittest 68 | 69 | 70 | def check(min_, max_, array): 71 | res = all([array[i] == 0 for i in xrange(min_, max_)]) 72 | return res 73 | 74 | 75 | def check_nondet(min_, max_, array): 76 | res = all([array[i] == 0 for i in xrange(min_, max_)]) 77 | if random.randint(0, 100) < 10: 78 | return False 79 | return res 80 | 81 | 82 | class TestColorizer(unittest.TestCase): 83 | 84 | def check_fuzz_result(self, i, testcase): 85 | color_info = [0] * len(testcase) 86 | c = ColorizerStrategy(len(testcase), lambda min_, max_: check(min_, max_, testcase)) 87 | while len(c.unknown_ranges) > 0: 88 | # print c.unknown_ranges 89 | c.colorize_step() 90 | print("det:", c.color_info) 91 | self.assertEqual([(0 if x == 1 else 1) for x in c.color_info], testcase) 92 | assert (all([x != 0 for x in c.color_info])) 93 | 94 | def check_nondet_fuzz_result(self, i, testcase): 95 | color_info = [0] * len(testcase) 96 | c = ColorizerStrategy(len(testcase), lambda min_, max_: check_nondet(min_, max_, testcase)) 97 | while len(c.unknown_ranges) > 0: 98 | offset = c.colorize_step() 99 | print("nondet:", c.color_info) 100 | if not all([x != 0 for x in c.color_info]): 101 | assert (False) 102 | 103 | def test_colorize_step_fuzzed(self): 104 | self.check_fuzz_result(1, [0]) 105 | self.check_fuzz_result(1, [0, 0, 1, 0]) 106 | self.check_fuzz_result(0, [0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1]) 107 | 108 | def test_fuzz_colorize_step(self): 109 | for i in xrange(0, 1000): 110 | random.seed(i) 111 | tlen = random.randint(1, 40) 112 | testcase = [0] * tlen 113 | num_ones = random.randint(0, (tlen - 1)) 114 | while num_ones > 0: 115 | r = random.randint(0, tlen - 1) 116 | if testcase[r] == 0: 117 | testcase[r] = 1 118 | num_ones -= 1 119 | print("testcase", testcase) 120 | self.check_fuzz_result(i, testcase) 121 | self.check_nondet_fuzz_result(i, testcase) 122 | 123 | 124 | if __name__ == '__main__': 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/encoding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import struct 18 | from itertools import product 19 | 20 | 21 | class Encoding: 22 | def to_intval(self, cmp, val): 23 | unpack_keys = {1: "B", 2: "H", 4: "L", 8: "Q"} 24 | key = unpack_keys.get(cmp.size / 8, None) 25 | if key: 26 | if self.signed: 27 | return struct.unpack("<" + key.lower(), val)[0] 28 | else: 29 | return struct.unpack("<" + key, val)[0] 30 | assert (false) 31 | 32 | def apply_reverse(self, val): 33 | if self.reverse: 34 | return val[::-1] 35 | return val 36 | 37 | def rev_desc(self): 38 | if self.reverse: 39 | return "r" 40 | return "p" 41 | 42 | def size(self): 43 | return 1 44 | 45 | def is_redundant(self, cmp, lhs, rhs): 46 | return False 47 | 48 | 49 | class SextEncoding(Encoding): 50 | def __init__(self, bytes, reverse): 51 | self.bytes = bytes 52 | self.reverse = reverse 53 | 54 | @staticmethod 55 | def _is_applicable_sext(cmp, size_bytes, val): 56 | check_size = cmp.size > 8 * size_bytes 57 | head, tail = val[0:len(val) - size_bytes], val[len(val) - size_bytes:] 58 | check_zeros = head == "\0" * len(head) and ord(tail[0]) & 0x80 == 0 59 | check_ffs = head == "\xff" * len(head) and ord(tail[0]) & 0x80 != 1 60 | return check_size and (check_zeros or check_ffs) 61 | 62 | def is_applicable(self, cmp, lhs, rhs): 63 | if cmp.type == "STR": 64 | return False 65 | lhs = self.apply_reverse(lhs) 66 | rhs = self.apply_reverse(rhs) 67 | return SextEncoding._is_applicable_sext(cmp, self.bytes, lhs) and SextEncoding._is_applicable_sext(cmp, 68 | self.bytes, 69 | rhs) 70 | 71 | def encode(self, cmp, val): 72 | return [val[len(val) - self.bytes:len(val)]] 73 | 74 | def name(self): 75 | return "sext_%s_%d" % (self.rev_desc(), self.bytes) 76 | 77 | # def is_redundant(self, cmp, lhs, rhs): 78 | # lhs = self.apply_reverse(lhs) 79 | # rhs = self.apply_reverse(rhs) 80 | # return not (ZextEncoding._is_applicable_zext(cmp,self.bytes, lhs) and ZextEncoding._is_applicable_zext(cmp, self.bytes, rhs)) 81 | 82 | 83 | class ZextEncoding(Encoding): 84 | def __init__(self, bytes, reverse): 85 | self.bytes = bytes 86 | self.reverse = reverse 87 | 88 | @staticmethod 89 | def _is_applicable_zext(cmp, size_bytes, val): 90 | return cmp.size > 8 * size_bytes and val[0:len(val) - size_bytes] == "\0" * (len(val) - size_bytes) 91 | 92 | def is_applicable(self, cmp, lhs, rhs): 93 | if cmp.type == "STR": 94 | return False 95 | lhs = self.apply_reverse(lhs) 96 | rhs = self.apply_reverse(rhs) 97 | return ZextEncoding._is_applicable_zext(cmp, self.bytes, lhs) and ZextEncoding._is_applicable_zext(cmp, 98 | self.bytes, 99 | rhs) 100 | 101 | # def is_redundant(self, cmp, lhs, rhs): 102 | # if self.bytes > 1: 103 | # lhs = self.apply_reverse(lhs) 104 | # rhs = self.apply_reverse(rhs) 105 | # return not (ZextEncoding._is_applicable_zext(cmp,self.bytes/2, lhs) and ZextEncoding._is_applicable_zext(cmp, self.bytes/2, rhs)) 106 | # return False 107 | 108 | def encode(self, cmp, val): 109 | return [val[len(val) - self.bytes:len(val)]] 110 | 111 | def name(self): 112 | return "zext_%s_%d" % (self.rev_desc(), self.bytes) 113 | 114 | 115 | class AsciiEncoding(Encoding): 116 | def __init__(self, base, signed): 117 | self.base = base 118 | self.signed = signed 119 | 120 | def is_applicable(self, cmp, lhs, rhs): 121 | return cmp.type != "STR" 122 | 123 | def encode(self, cmp, val): 124 | intval = self.to_intval(cmp, val) 125 | if self.base == 16: 126 | return ["%x" % intval] 127 | if self.base == 10: 128 | return ["%d" % intval] 129 | if self.base == 8: 130 | return ["%o" % intval] 131 | assert (False) 132 | 133 | def name(self): 134 | sign = "u" 135 | if self.signed: 136 | sign = "s" 137 | return "ascii_%s_%d" % (sign, self.base) 138 | 139 | 140 | class MemEncoding(Encoding): 141 | def __init__(self, length): 142 | self.length = length 143 | 144 | def is_applicable(self, cmp, lhs, rhs): 145 | if lhs[0:self.length].count("\0") > self.length / 2 or rhs[0:self.length].count("\0") > self.length / 2: 146 | return False 147 | return cmp.type == "STR" 148 | 149 | def encode(self, cmp, val): 150 | return [val[0:self.length]] 151 | 152 | def name(self): 153 | return "mem_%d" % (self.length) 154 | 155 | 156 | class CStringEncoding(Encoding): 157 | 158 | def is_applicable(self, cmp, lhs, rhs): 159 | if len(lhs) < 2 or len(rhs) < 2: 160 | return False 161 | non_null1 = lhs[0] != "\0" and rhs[0] != "\0" 162 | non_null2 = lhs[1] != "\0" and rhs[1] != "\0" 163 | return cmp.type == "STR" and non_null1 and non_null2 164 | 165 | def encode(self, cmp, val): 166 | if "\0" in val: 167 | return [val[0:max(2, val.find("\0"))]] 168 | else: 169 | return [val] 170 | 171 | def name(self): 172 | return "cstr" 173 | 174 | 175 | class CStrChrEncoding(Encoding): 176 | 177 | def __init__(self, amount=0): 178 | self.amount = amount 179 | 180 | def is_applicable(self, cmp, lhs, rhs): 181 | if len(lhs) <= self.amount or len(rhs) < 2: 182 | return False 183 | non_null2 = rhs[0] != "\0" and rhs[1:] == "\0" * (len(rhs) - 1) 184 | return cmp.type == "STR" and non_null2 185 | 186 | def encode(self, cmp, val): 187 | if val[1] == "\0" and val[0] != "\0": 188 | return val[0] 189 | return val[self.amount] 190 | 191 | def name(self): 192 | return "cstrchr_%d" % self.amount 193 | 194 | 195 | class PlainEncoding(Encoding): 196 | def __init__(self, reverse): 197 | self.reverse = reverse 198 | 199 | def is_applicable(self, cmp, lhs, rhs): 200 | return cmp.type != "STR" 201 | 202 | def encode(self, cmp, val): 203 | return [self.apply_reverse(val)] 204 | 205 | # def is_redundant(self, cmp, lhs, rhs): 206 | # if cmp.type == "STR": 207 | # return False 208 | # lhs = self.apply_reverse(lhs) 209 | # rhs = self.apply_reverse(rhs) 210 | # return ZextEncoding._is_applicable_zext(cmp, cmp.size*4, lhs) and ZextEncoding._is_applicable_zext(cmp, cmp.size*4, rhs) 211 | 212 | def name(self): 213 | return "plain_%s" % (self.rev_desc()) 214 | 215 | 216 | class SplitEncoding(Encoding): 217 | def __init__(self, len, reverse): 218 | assert (len == 8) 219 | self.reverse = reverse 220 | 221 | def is_applicable(self, cmp, lhs, rhs): 222 | return cmp.size == 64 223 | 224 | def encode(self, cmp, val): 225 | val = self.apply_reverse(val) 226 | return [val[0:4], val[4:8]] 227 | 228 | def name(self): 229 | return "split_%s" % (self.rev_desc()) 230 | 231 | def size(self): 232 | return 2 233 | 234 | def is_redundant(self, cmp, lhs, rhs): 235 | # unhandled corner case: split where 00000000 is split in uncolorized version, but colorized version is actually 236 | # informative 237 | zeros = "\0\0\0\0" 238 | low = (lhs[:4] == zeros and rhs[:4] == zeros) 239 | high = (lhs[4:] == zeros and rhs[4:] == zeros) 240 | # log_redq("is redundant %s %s %s %s"%(lhs, rhs, low, high)) 241 | return low or high 242 | 243 | 244 | class R1E(Encoding): 245 | def __init__(self, orig): 246 | self.orig = orig 247 | 248 | def is_applicable(self, cmp, lhs, rhs): 249 | res = self.orig.is_applicable(cmp, lhs, rhs) 250 | # if cmp.addr == 4196216: 251 | # print(repr(("is_applicable", cmp.addr,lhs, rhs, "=", res))) 252 | return res 253 | 254 | def encode(self, cmp, val): 255 | res = map(self.r1, self.orig.encode(cmp, val)) 256 | # if cmp.addr == 4196216: 257 | # print(repr(("encode",val,"to",res))) 258 | return res 259 | 260 | def name(self): 261 | return "R1(%s)" % self.orig.name() 262 | 263 | def r1(self, str): 264 | return "".join(map(lambda x: chr((ord(x) + 1) % 256), str)) 265 | 266 | 267 | Encoders = [ZextEncoding(bytes, reverse) for (bytes, reverse) in product([1, 2, 4], [True, False])] + \ 268 | [SextEncoding(bytes, reverse) for (bytes, reverse) in product([1, 2, 4], [True, False])] + \ 269 | [AsciiEncoding(base, signed) for (base, signed) in product([8, 10, 16], [True, False])] + \ 270 | [PlainEncoding(True), PlainEncoding(False)] + \ 271 | [SplitEncoding(8, True), SplitEncoding(8, False)] + \ 272 | [CStringEncoding()] + \ 273 | [MemEncoding(length) for length in [4, 5, 6, 7, 8, 16, 32]] # +\ 274 | # [ R1E(ZextEncoding(bytes, reverse)) for (bytes, reverse) in product([4], [True, False]) ] + \ 275 | # [ R1E(SextEncoding(bytes, reverse)) for (bytes, reverse) in product([4], [True, False]) ] + \ 276 | # [ R1E(PlainEncoding(True)), R1E(PlainEncoding(False)) ] + \ 277 | # [ R1E(CStringEncoding()) ] + \ 278 | # [ R1E(MemEncoding(length)) for length in [4, 5, 6, 7, 8, 16, 32] ] # +\ 279 | # [ CStrChrEncoding(length) for length in xrange(0,4)] 280 | 281 | # Encoders = [ CStrChrEncoding(length) for length in xrange(0,4)] 282 | 283 | import unittest 284 | 285 | 286 | class Dummy: 287 | pass 288 | 289 | 290 | class TestEncoder(unittest.TestCase): 291 | def test_applicable(self): 292 | a = Dummy() 293 | a.size = 32 294 | assert (Encoding.is_applicable(a, "p_zext1", "\0\0\0a")) 295 | assert (not Encoding.is_applicable(a, "p_zext1", "\0\0ba")) 296 | assert (not Encoding.is_applicable(a, "r_zext1", "\0\0\0a")) 297 | assert (Encoding.is_applicable(a, "r_zext1", "\0\0\0a")) 298 | 299 | 300 | if __name__ == '__main__': 301 | unittest.main() 302 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/hash_fix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import traceback 18 | from array import array 19 | 20 | import parser 21 | from common.debug import log_redq 22 | from parser import Cmp, RedqueenRunInfo 23 | 24 | MAX_NUMBER_PERMUTATIONS = 100000 25 | 26 | 27 | class HashFixer: 28 | 29 | def __init__(self, qemu, rq_state): 30 | self.qemu = qemu 31 | self.addrs = rq_state.get_candidate_hash_addrs() 32 | self.redqueen_state = rq_state 33 | self.blacklisted_addrs = set() 34 | 35 | def get_broken_cmps(self, data): 36 | broken_cmps = [] 37 | res, run_info = self.get_cmps(data) 38 | for addr in res: 39 | for cmp in res[addr]: 40 | if not cmp.was_true_in(run_info) and not cmp.addr in self.blacklisted_addrs: 41 | broken_cmps.append(cmp) 42 | return broken_cmps, run_info 43 | 44 | def get_cmps(self, data): 45 | # log_redq("runnning on %s"%repr("".join( map(chr, data) )) ) 46 | self.qemu.set_payload(data) 47 | # self.qemu.send_enable_patches() 48 | log_redq("hashfix run in rq mode") 49 | self.qemu.send_rq_set_whitelist_instrumentation() 50 | self.qemu.send_enable_redqueen() 51 | self.qemu.send_payload(timeout_detection=True, apply_patches=True) 52 | log_redq("hashfix run in non rq mode") 53 | self.qemu.send_disable_redqueen() 54 | self.qemu.send_payload(timeout_detection=True, apply_patches=True) 55 | log_redq("hashfix done running, now parsing") 56 | res = self.parse_redqueen_results(data) 57 | log_redq("hashfix done parsing") 58 | return res 59 | 60 | def mark_unfixable(self, cmp): 61 | log_redq("Unfixable cmp at: %x" % cmp.addr) 62 | self.blacklisted_addrs.add(cmp.addr) 63 | self.redqueen_state.blacklist_hash_addr(cmp.addr) 64 | 65 | def get_shape(self, redqueen_results): 66 | res = {} 67 | for addr in redqueen_results: 68 | res[addr] = len(redqueen_results[addr]) 69 | return res 70 | 71 | def try_fix_data(self, data): 72 | self.qemu.send_payload(timeout_detection=True, apply_patches=False) 73 | self.qemu.send_payload(timeout_detection=True, apply_patches=True) 74 | log_redq("PATCHES %s\n" % repr(map(hex, self.redqueen_state.get_candidate_hash_addrs()))) 75 | log_redq("BLACKLIST %s\n" % repr(map(hex, self.redqueen_state.get_blacklisted_hash_addrs()))) 76 | self.redqueen_state.update_redqueen_patches(self.qemu.redqueen_workdir) 77 | self.redqueen_state.update_redqueen_whitelist(self.qemu.redqueen_workdir, 78 | self.redqueen_state.get_candidate_hash_addrs()) 79 | fixed_data = array('B', data) 80 | orig_cmps, _ = self.get_cmps(fixed_data) 81 | shape = self.get_shape(orig_cmps) 82 | log_redq("shape of hashes: ") 83 | for addr in shape: 84 | log_redq("\t%x: %d" % (addr, shape[addr])) 85 | 86 | if len(shape) == 0: 87 | return fixed_data 88 | 89 | num_iters = min(len(orig_cmps) ** 2 + 1, len(orig_cmps) * 3 + 1) 90 | num_cmps = sum(shape.values()) + 1 91 | if num_iters < num_cmps: 92 | num_iters = num_cmps 93 | 94 | log_redq("try fixing for %d iters" % num_iters) 95 | for i in range(num_iters): 96 | broken_checks, run_info = self.get_broken_cmps(fixed_data) 97 | log_redq("got %d broken checks\n" % len(broken_checks)) 98 | if not broken_checks: 99 | return fixed_data 100 | cmp = broken_checks.pop(-1); 101 | if not self.try_fix_cmp(shape, fixed_data, run_info, cmp): 102 | log_redq("cmp at %x unfixable:" % cmp.addr) 103 | self.mark_unfixable(cmp) 104 | broken_checks, run_info = self.get_broken_cmps(fixed_data) 105 | for cmp in broken_checks: 106 | self.mark_unfixable(cmp) 107 | return False 108 | 109 | def parse_redqueen_results(self, data): 110 | res = {} 111 | rq_res = parser.read_file(self.qemu.redqueen_workdir.redqueen()) 112 | data_string = "".join(map(chr, data)) 113 | run_info = RedqueenRunInfo(1, False, rq_res, data_string) 114 | for line in run_info.hook_info.splitlines(): 115 | addr, type, size, is_imm, lhs, rhs = parser.RedqueenInfo.parse_line(line) 116 | assert (type == "CMP") 117 | res[addr] = res.get(addr, []) 118 | cmp = Cmp(addr, type, size, is_imm) 119 | cmp.index = len(res[addr]) 120 | res[addr].append(cmp) 121 | cmp.add_result(run_info, lhs, rhs) 122 | return res, run_info 123 | 124 | @staticmethod 125 | def replace_data(data, offset, repl): 126 | for o in range(len(repl)): 127 | data[offset + o] = repl[o] 128 | 129 | def try_fix_cmp_with(self, shape, fixed_data, cmp, offsets, lhs, rhs, enc): 130 | log_redq("Trying mutation %s" % (repr((offsets, lhs, rhs, enc)))) 131 | if map(len, lhs) != map(len, rhs): 132 | return False 133 | self.redqueen_state.update_redqueen_whitelist(self.qemu.redqueen_workdir, 134 | self.redqueen_state.get_candidate_hash_addrs()) 135 | try: 136 | if self.try_fix_cmp_offset(shape, fixed_data, cmp, offsets, rhs): 137 | log_redq("Mutation fixed it") 138 | return True 139 | log_redq("Mutation didn't Fix it") 140 | return False 141 | except Exception as e: 142 | log_redq("fixing hash failed %s" % traceback.format_exc()) 143 | raise e 144 | 145 | def try_fix_cmp(self, shape, fixed_data, run_info, cmp): 146 | known_offsets = self.redqueen_state.get_candidate_file_offsets(cmp.addr) 147 | log_redq("known offsets for: %x = %s" % (cmp.addr, known_offsets)) 148 | mutations = [x for x in cmp.calc_mutations(run_info, 1)] 149 | for (offsets, lhs, rhs, enc) in cmp.calc_mutations(run_info, 1): 150 | if offsets in known_offsets: 151 | if self.try_fix_cmp_with(shape, fixed_data, cmp, offsets, lhs, rhs, enc): 152 | return True 153 | for (offsets, lhs, rhs, enc) in cmp.calc_mutations(run_info, 1): 154 | if not offsets in known_offsets: 155 | if self.try_fix_cmp_with(shape, fixed_data, cmp, offsets, lhs, rhs, enc): 156 | return True 157 | return False 158 | 159 | def does_data_fix_cmp(self, shape, data, cmp): 160 | res, run_info = self.get_cmps(data) 161 | return shape == self.get_shape(res) and res[cmp.addr][cmp.index].was_true_in(run_info) 162 | 163 | def try_fix_cmp_offset(self, shape, data, cmp, offsets, repls): 164 | # try: 165 | backup = {} 166 | for i, repl in zip(offsets, repls): 167 | backup[i] = data[i:i + len(repl)] 168 | HashFixer.replace_data(data, i, array('B', repl)) 169 | if self.does_data_fix_cmp(shape, data, cmp): 170 | log_redq("found candidate offset %x %s" % (cmp.addr, repr(offsets))) 171 | self.redqueen_state.add_candidate_file_offset(cmp.addr, tuple(offsets)) 172 | return True 173 | for i in offsets: 174 | HashFixer.replace_data(data, i, backup[i]) 175 | return False 176 | # except Exception as e: 177 | # log_redq("failed to fix %s with %s"%(cmp.addr,(offset_tuples,repls)) ) 178 | # raise e 179 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/hash_patch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | 19 | class HashPatcher: 20 | def __init__(self): 21 | self.patched = set() 22 | self.blacklisted = set() 23 | 24 | def add_hash_candidate(self, mut): 25 | if mut.addr in self.blacklisted or mut.addr in self.patched: 26 | return 27 | self.patched.add(mut.addr) 28 | self.apply_patches() 29 | 30 | def blacklist_hash_candidate(self, addr): 31 | self.blacklisted.add(addr) 32 | if addr in self.patched: 33 | self.patched.remove(addr) 34 | self.apply_patches() 35 | 36 | def apply_patches(self): 37 | with open("/tmp/redqueen_whitelist", "w") as w: 38 | with open("/tmp/rq_patches", "w") as f: 39 | for addr in self.patched: 40 | hexaddr = hex(addr).rstrip("L").lstrip("0x") 41 | if hexaddr: 42 | w.write(hexaddr + "\n") 43 | f.write(hexaddr + "\n") 44 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/mod.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import os.path 18 | from array import array 19 | from shutil import copyfile, rmtree 20 | 21 | import parser 22 | from common.debug import log_redq 23 | 24 | MAX_NUMBER_PERMUTATIONS = 1000 # number of trials per address, lhs and encoding 25 | 26 | 27 | class RedqueenInfoGatherer: 28 | def __init__(self): 29 | self.num_alternative_inputs = 0 30 | self.collected_infos_path = None 31 | self.workdir = None 32 | self.num_mutations = 0 33 | self.verbose = False 34 | 35 | def make_paths(self, workdir): 36 | self.workdir = workdir 37 | self.collected_infos_path = workdir.base_path + "/collected_infos" 38 | rmtree(self.collected_infos_path, ignore_errors=True) 39 | os.mkdir(self.collected_infos_path) 40 | 41 | def get_info(self, input_data): 42 | self.num_alternative_inputs += 1 43 | self.save_rq_data(self.num_alternative_inputs, input_data) 44 | print("redqueen saving stuff....") 45 | with open(self.collected_infos_path + "/input_%d.bin" % (self.num_alternative_inputs), "wb") as f: 46 | f.write(input_data) 47 | 48 | def save_rq_data(self, id, data): 49 | if os.path.exists(self.workdir.redqueen()): 50 | copyfile(self.workdir.redqueen(), "%s/redqueen_result_%d.txt" % (self.collected_infos_path, id)) 51 | # copyfile(self.workdir.code_dump(),"%s/redqueen_vm.img"%(self.collected_infos_path)) 52 | 53 | def __get_redqueen_proposals(self): 54 | num_colored_versions = self.num_alternative_inputs 55 | orig_id = self.num_alternative_inputs 56 | rq_info, (num_mutations, offset_to_lhs_to_rhs_to_info) = parser.parse_rq(self.collected_infos_path, 57 | num_colored_versions, orig_id) 58 | self.rq_info = rq_info 59 | self.rq_offsets_to_lhs_to_rhs_to_info = offset_to_lhs_to_rhs_to_info 60 | self.num_mutations += num_mutations 61 | 62 | def get_hash_candidates(self): 63 | return self.rq_info.get_hash_candidates() 64 | 65 | def get_boring_cmps(self): 66 | return self.rq_info.boring_cmps 67 | 68 | def get_proposals(self): 69 | self.__get_redqueen_proposals() 70 | 71 | def enumerate_mutations(self): 72 | for offsets in self.rq_offsets_to_lhs_to_rhs_to_info: 73 | for lhs in self.rq_offsets_to_lhs_to_rhs_to_info[offsets]: 74 | for rhs in self.rq_offsets_to_lhs_to_rhs_to_info[offsets][lhs]: 75 | yield (offsets, lhs, rhs, self.rq_offsets_to_lhs_to_rhs_to_info[offsets][lhs][rhs]) 76 | 77 | def run_mutate_redqueen(self, payload_array, func, default_info): 78 | for (offset, lhs, rhs, info) in self.enumerate_mutations(): 79 | if self.verbose: 80 | log_redq("redqueen fuzz data %s" % repr((offset, lhs, rhs, info))) 81 | 82 | def run(data): 83 | default_info["redqueen"] = [repr(lhs), repr(rhs)] + list(info.infos) 84 | func(data, default_info) 85 | 86 | RedqueenInfoGatherer.fuzz_data(payload_array, run, offset, lhs, rhs) 87 | 88 | def get_num_mutations(self): 89 | return self.num_mutations 90 | 91 | @staticmethod 92 | def replace_data(data, offset, repl): 93 | for o in range(len(repl)): 94 | data[offset + o] = repl[o] 95 | 96 | @staticmethod 97 | def fuzz_data_same_len(data, func, offset_tuple, repl_tuple): 98 | backup = {} 99 | for i, repl in zip(offset_tuple, repl_tuple): 100 | for j in xrange(i, i + len(repl)): 101 | backup[j] = data[j] 102 | 103 | for i, repl in zip(offset_tuple, repl_tuple): 104 | RedqueenInfoGatherer.replace_data(data, i, array('B', repl)) 105 | func(data.tostring()) 106 | for i in backup: 107 | data[i] = backup[i] 108 | 109 | @staticmethod 110 | def fuzz_data_different_len(data, func, offset_tuple, pat_length_tuple, repl_tuple): 111 | res_str = "" 112 | last_offset = 0 113 | for i, orig_length, repl in zip(sorted(offset_tuple), pat_length_tuple, repl_tuple): 114 | res_str += data[last_offset:i].tostring() 115 | res_str += repl 116 | last_offset = i + orig_length 117 | res_str += data[last_offset:].tostring() 118 | func(res_str) 119 | 120 | @staticmethod 121 | def fuzz_data(data, func, offset_tuple, pat_tuple, repl_tuple): 122 | pat_len_tuple = map(len, pat_tuple) 123 | if pat_len_tuple != map(len, repl_tuple): 124 | RedqueenInfoGatherer.fuzz_data_different_len(data, func, offset_tuple, pat_len_tuple, repl_tuple) 125 | else: 126 | RedqueenInfoGatherer.fuzz_data_same_len(data, func, offset_tuple, repl_tuple) 127 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | import re 18 | 19 | from cmp import Cmp 20 | 21 | 22 | # import ipdb 23 | 24 | 25 | def read_file(path): 26 | with open(path, 'r') as content_file: 27 | return content_file.read() 28 | 29 | 30 | class RedqueenRunInfo: 31 | def __init__(self, id, was_colored, hook_info, input_data): 32 | self.id = id 33 | self.hook_info = hook_info 34 | self.input_data = input_data 35 | self.pattern_to_offsets = {} 36 | self.was_colored = was_colored 37 | 38 | def get_offset_tuple(self, pattern_tuple): 39 | return tuple([self.get_offsets(pat) for pat in pattern_tuple]) 40 | 41 | def get_offsets(self, pattern): 42 | if pattern in self.pattern_to_offsets: 43 | return set(self.pattern_to_offsets[pattern]) 44 | self.pattern_to_offsets[pattern] = self.calc_offsets(pattern) 45 | return set(self.pattern_to_offsets[pattern]) 46 | 47 | def calc_offsets(self, pattern): 48 | res = set() 49 | start = 0 50 | while True: 51 | start = self.input_data.find(pattern, start) 52 | if start == -1: 53 | return res 54 | res.add(start) 55 | start += 1 # use start += 1 to find overlapping matches 56 | 57 | 58 | class RedqueenInfo: 59 | def __init__(self): 60 | self.addr_to_cmp = {} 61 | self.addr_to_inv_cmp = {} 62 | self.run_infos = set() 63 | self.boring_cmps = set() 64 | 65 | def load(self, input_id, was_colored, path): 66 | hook_info = read_file("%s/redqueen_result_%d.txt" % (path, input_id)) 67 | bin_info = read_file("%s/input_%d.bin" % (path, input_id)) 68 | return self.load_data(input_id, was_colored, hook_info, bin_info) 69 | 70 | def load_data(self, input_id, was_colored, hook_info, bin_info): 71 | run_info = RedqueenRunInfo(input_id, was_colored, hook_info, bin_info) 72 | self.run_infos.add(run_info) 73 | self.parse_run_info(run_info) 74 | return run_info 75 | 76 | def parse_run_info(self, run_info): 77 | self.run_infos.add(run_info) 78 | for line in run_info.hook_info.splitlines(): 79 | self.parse_line_and_update_compares(run_info, line) 80 | 81 | @staticmethod 82 | def parse_line(line): 83 | m = re.search( 84 | r'([a-fA-F0-9]+)\s+(CMP|SUB|STR|LEA)\s+(8|16|32|64|512)\s+([a-fA-F0-9]+)\s*-\s*([a-fA-F0-9]+)\s*(IMM)?', 85 | line) 86 | assert (m) 87 | addr = int(m.group(1), 16) 88 | type = m.group(2) 89 | size = int(m.group(3)) 90 | is_imm = not not m.group(6) 91 | lhs = m.group(4).decode('hex') 92 | rhs = m.group(5).decode('hex') 93 | return addr, type, size, is_imm, lhs, rhs 94 | 95 | def add_run_result(self, run_info, addr, type, size, is_imm, lhs, rhs, addr_to_cmp): 96 | addr_to_cmp[addr] = addr_to_cmp.get(addr, Cmp(addr, type, size, is_imm)) 97 | cmp = addr_to_cmp[addr] 98 | assert (cmp.addr == addr) 99 | assert (cmp.type == type) 100 | assert (cmp.size == size) 101 | assert (cmp.is_imm == is_imm) 102 | assert (len(lhs) == size / 8) 103 | assert (len(rhs) == size / 8) 104 | cmp.add_result(run_info, lhs, rhs) 105 | 106 | def parse_line_and_update_compares(self, run_info, line): 107 | addr, type, size, is_imm, lhs, rhs = RedqueenInfo.parse_line(line) 108 | self.add_run_result(run_info, addr, type, size, is_imm, lhs, rhs, self.addr_to_cmp) 109 | if not is_imm: 110 | self.add_run_result(run_info, addr, type, size, is_imm, rhs, lhs, self.addr_to_inv_cmp) 111 | 112 | def get_all_mutations(self): 113 | orig_run_info = [r for r in self.run_infos if not r.was_colored] 114 | assert (len(orig_run_info) == 1) 115 | self.boring_cmps = set() 116 | orig_run_info = orig_run_info[0] 117 | offsets_to_lhs_to_rhs_to_info = {} 118 | num_mut = 0 119 | for addr_to_cmp in [self.addr_to_cmp, self.addr_to_inv_cmp]: 120 | for addr in addr_to_cmp: 121 | cmp = addr_to_cmp[addr] 122 | was_cmp_interessting = False 123 | if len(cmp.run_info_to_pairs) == len(self.run_infos): 124 | for (offsets, lhs, rhs, encoding) in cmp.calc_mutations(orig_run_info, len(self.run_infos)): 125 | offsets, lhs, rhs = self.strip_unchanged_bytes_from_mutation_values(offsets, lhs, rhs) 126 | was_cmp_interessting = True 127 | offsets_to_lhs_to_rhs_to_info[offsets] = offsets_to_lhs_to_rhs_to_info.get(offsets, {}) 128 | offsets_to_lhs_to_rhs_to_info[offsets][lhs] = offsets_to_lhs_to_rhs_to_info[offsets].get(lhs, 129 | {}) 130 | if not rhs in offsets_to_lhs_to_rhs_to_info[offsets][lhs]: 131 | num_mut += 1 132 | offsets_to_lhs_to_rhs_to_info[offsets][lhs][rhs] = offsets_to_lhs_to_rhs_to_info[offsets][ 133 | lhs].get(rhs, MutInfo()) 134 | offsets_to_lhs_to_rhs_to_info[offsets][lhs][rhs].add_info(addr, encoding) 135 | if not was_cmp_interessting: 136 | self.boring_cmps.add(cmp.addr) 137 | return num_mut, offsets_to_lhs_to_rhs_to_info 138 | 139 | def strip_unchanged_bytes_from_mutation(self, offset, lhs, rhs): 140 | assert (len(lhs) == len(rhs)) 141 | i = 0 142 | ll = len(lhs) 143 | res_lhss, res_rhss, res_offsets = [], [], [] 144 | while i < ll: 145 | j = i 146 | while j < ll and lhs[j] != rhs[j]: 147 | j += 1 148 | if j != i: 149 | res_lhss.append(lhs[i:j]) 150 | res_rhss.append(rhs[i:j]) 151 | res_offsets.append(offset + i) 152 | i = j + 1 153 | return res_offsets, res_lhss, res_rhss 154 | 155 | def strip_unchanged_bytes_from_mutation_values(self, offsets, lhss, rhss): 156 | assert (len(offsets) == len(lhss)) 157 | assert (len(offsets) == len(rhss)) 158 | res_offsets, res_lhss, res_rhss = [], [], [] 159 | for i in range(len(offsets)): 160 | if len(lhss[i]) != len(rhss[i]): 161 | res_offsets.append(offsets[i]) 162 | res_lhss.append(lhss[i]) 163 | res_rhss.append(rhss[i]) 164 | else: 165 | new_offsets, new_lhss, new_rhss = self.strip_unchanged_bytes_from_mutation(offsets[i], lhss[i], rhss[i]) 166 | res_offsets += new_offsets 167 | res_lhss += new_lhss 168 | res_rhss += new_rhss 169 | # log_redq("strip: %s -> %s"%( repr((offsets,lhss,rhss)), repr((res_offsets,res_lhss, res_rhss)) ) ) 170 | return tuple(res_offsets), tuple(res_lhss), tuple(res_rhss) 171 | 172 | def get_hash_candidates(self): 173 | res = set() 174 | for addr in self.addr_to_cmp: 175 | cmp = self.addr_to_cmp[addr] 176 | if not cmp in self.boring_cmps and cmp.could_be_hash(): 177 | res.add(addr) 178 | return res 179 | 180 | 181 | class MutInfo: 182 | def __init__(self): 183 | self.infos = set() 184 | 185 | def add_info(self, addr, encoding): 186 | self.infos.add((addr, encoding.name(),)) 187 | 188 | def __repr__(self): 189 | return "MutInfo<%s>" % (repr(self.infos)) 190 | 191 | 192 | def parse_rq(path, num_files, orig_file_id): 193 | rq_info = RedqueenInfo() 194 | for id in range(1, num_files + 1): 195 | rq_info.load(id, id != orig_file_id, path) 196 | return rq_info, rq_info.get_all_mutations() 197 | 198 | 199 | def parse_rq_data(hook_data, input_data): 200 | rq_info = RedqueenInfo() 201 | rq_info.load_data(1, False, hook_data, input_data) 202 | return rq_info.get_all_mutations() 203 | -------------------------------------------------------------------------------- /fuzzer/technique/redqueen/workdir.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import os 19 | import shutil 20 | 21 | 22 | class RedqueenWorkdir: 23 | def __init__(self, qemu_id, config): 24 | #self.base_path = config.argument_values['work_dir'] + "/redqueen_workdir_" + str(qemu_id) 25 | self.base_path = "/tmp/redqueen_workdir_" + str(qemu_id) 26 | 27 | def init_dir(self): 28 | if os.path.exists(self.base_path): 29 | shutil.rmtree(self.base_path) 30 | os.makedirs(self.base_path) 31 | 32 | def redqueen(self): 33 | return self.base_path + "/redqueen_results.txt" 34 | 35 | def patches(self): 36 | return self.base_path + "/redqueen_patches.txt" 37 | 38 | def whitelist(self): 39 | return self.base_path + "/breakpoint_white.txt" 40 | 41 | def blacklist(self): 42 | return self.base_path + "/breakpoint_black.txt" 43 | 44 | def code_dump(self): 45 | return self.base_path + "/target_code_dump.img" 46 | -------------------------------------------------------------------------------- /fuzzer/technique/trim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | from fuzzer.bitmap import GlobalBitmap 18 | 19 | MAX_EXECS = 16 20 | MAX_ROUNDS = 32 21 | MIN_SIZE = 32 22 | APPEND_VALUE = 0.1 23 | 24 | APPEND_BYTES = 16 25 | 26 | pow2_values = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768] 27 | 28 | 29 | def get_pow2_value(value): 30 | for pow2_value in reversed(pow2_values): 31 | if pow2_value <= value: 32 | return pow2_value 33 | return 1 34 | 35 | 36 | def check_trim_still_valid(old_node, old_bitmap, new_bitmap): 37 | # non-det input 38 | if not new_bitmap: 39 | return False 40 | if not new_bitmap.is_lut_applied(): 41 | new_bitmap.apply_lut() 42 | trim_simple = False 43 | if trim_simple: 44 | assert False # todo fixme wrt to bitmaps, == doesnt work on bitmap_wrapper 45 | return old_bitmap == new_bitmap 46 | else: 47 | old_bits = old_node["new_bytes"].copy() 48 | old_bits.update(old_node["new_bits"]) 49 | return GlobalBitmap.all_new_bits_still_set(old_bits, new_bitmap) 50 | 51 | 52 | def perform_center_trim(payload, old_node, send_handler, default_info, error_handler, trimming_bytes): 53 | index = 0 54 | old_bitmap, _ = send_handler(payload, default_info) 55 | 56 | if error_handler(): 57 | return payload 58 | 59 | while index < len(payload): 60 | test_payload = payload[0: index] + payload[index + trimming_bytes:] 61 | 62 | new_bitmap, _ = send_handler(test_payload, default_info) 63 | 64 | # if error_handler(): 65 | # return payload 66 | 67 | if check_trim_still_valid(old_node, old_bitmap, new_bitmap): 68 | payload = test_payload[:] 69 | else: 70 | index += trimming_bytes 71 | 72 | return payload 73 | 74 | 75 | def perform_trim(payload, old_node, send_handler, default_info, error_handler): 76 | global MAX_ROUNDS, MAX_EXECS, MIN_SIZE, APPEND_BYTES 77 | if len(payload) <= MIN_SIZE: 78 | return payload 79 | 80 | old_bitmap, _ = send_handler(payload, default_info) 81 | if error_handler(): 82 | return payload 83 | execs = 0 84 | new_size = len(payload) 85 | 86 | for _ in xrange(MAX_ROUNDS): 87 | abort = True 88 | for i in reversed(xrange(0, pow2_values.index(get_pow2_value(new_size)) + 1)): 89 | if pow2_values[i] < new_size: 90 | 91 | execs += 1 92 | if execs == MAX_EXECS: 93 | abort = True 94 | break 95 | 96 | new_bitmap, _ = send_handler(payload[0:new_size - pow2_values[i]], default_info) 97 | 98 | if error_handler(): 99 | return payload[0:new_size] 100 | 101 | if check_trim_still_valid(old_node, old_bitmap, new_bitmap): 102 | new_size -= pow2_values[i] 103 | abort = False 104 | break 105 | 106 | if new_size <= MIN_SIZE: 107 | break 108 | 109 | if abort: 110 | break 111 | 112 | new_size_backup = new_size 113 | if new_size < MIN_SIZE: 114 | new_size = MIN_SIZE 115 | elif (new_size + int(new_size * APPEND_VALUE)) < len(payload): 116 | new_size += int(new_size * APPEND_VALUE) 117 | 118 | new_size += APPEND_BYTES 119 | 120 | new_bitmap, _ = send_handler(payload[0:new_size], default_info) 121 | if not check_trim_still_valid(old_node, old_bitmap, new_bitmap): 122 | return payload[0:min(new_size_backup, len(payload))] 123 | 124 | return payload[0:min(new_size, len(payload))] 125 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | __ __ ___ ________ 2 | / /_____ _________ ___ / / / | / ____/ / 3 | / //_/ _ \/ ___/ __ \/ _ \/ / / /| | / /_ / / 4 | / ,< / __/ / / / / / __/ / / ___ |/ __/ / /___ 5 | /_/|_|\___/_/ /_/ /_/\___/_/ /_/ |_/_/ /_____/ 6 | =================================================== 7 | 8 | kernel AFL: A feedback-driven general purpose ring-0 and ring-3 interface fuzzer for x86 / x86-64 code. 9 | 10 | Sergej Schumilo 11 | Cornelius Aschermann 12 | Tim Blazytko 13 | 14 | Version: 0.2 (Grimoire) 15 | 16 | (C) 2019 17 | -------------------------------------------------------------------------------- /info/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sergej' 2 | -------------------------------------------------------------------------------- /info/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | """ 17 | 18 | import os 19 | import time 20 | import shutil 21 | 22 | from common.config import InfoConfiguration 23 | from common.debug import log_info, enable_logging 24 | from common.qemu import qemu 25 | from common.self_check import post_self_check 26 | from common.util import prepare_working_dir 27 | 28 | DEFAULT_INFO_WORKDIR = "/tmp/workdir_info" 29 | 30 | def start(): 31 | config = InfoConfiguration() 32 | 33 | if not post_self_check(config): 34 | return -1 35 | 36 | config.argument_values["work_dir"] = DEFAULT_INFO_WORKDIR 37 | 38 | if config.argument_values['v']: 39 | enable_logging(config.argument_values["work_dir"]) 40 | 41 | prepare_working_dir(config.argument_values['work_dir']) 42 | 43 | log_info("Dumping target addresses...") 44 | if os.path.exists("/tmp/kAFL_info.txt"): 45 | os.remove("/tmp/kAFL_info.txt") 46 | q = qemu(0, config) 47 | q.start() 48 | q.__del__() 49 | try: 50 | for line in open("/tmp/kAFL_info.txt"): 51 | print line, 52 | os.remove("/tmp/kAFL_info.txt") 53 | except: 54 | pass 55 | 56 | shutil.rmtree(DEFAULT_INFO_WORKDIR) 57 | return 0 58 | -------------------------------------------------------------------------------- /kafl_debug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | import os 20 | import sys 21 | 22 | import common.color 23 | from common.self_check import self_check 24 | 25 | 26 | def main(): 27 | f = open(os.path.dirname(sys.argv[0]) + "/help.txt") 28 | for line in f: 29 | print(line.replace("\n", "")) 30 | f.close() 31 | 32 | print("<< " + common.color.BOLD + common.color.OKGREEN + sys.argv[ 33 | 0] + ": kAFL Agent Debugger " + common.color.ENDC + ">>\n") 34 | 35 | if not self_check(): 36 | return 1 37 | 38 | from debug.core import start 39 | return start() 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /kafl_fuzz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | import os 20 | import sys 21 | 22 | import common.color 23 | from common.self_check import self_check 24 | 25 | __author__ = 'sergej' 26 | 27 | 28 | def main(): 29 | f = open(os.path.dirname(sys.argv[0]) + "/help.txt") 30 | for line in f: 31 | print(line.replace("\n", "")) 32 | f.close() 33 | 34 | print("<< " + common.color.BOLD + common.color.OKGREEN + sys.argv[ 35 | 0] + ": Kernel Fuzzer " + common.color.ENDC + ">>\n") 36 | 37 | if not self_check(): 38 | return 1 39 | 40 | from fuzzer.core import start 41 | return start() 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /kafl_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import sys 21 | 22 | import common.color 23 | from common.self_check import self_check 24 | 25 | __author__ = 'sergej' 26 | 27 | 28 | def main(): 29 | f = open("help.txt") 30 | for line in f: 31 | print(line.replace("\n", "")) 32 | f.close() 33 | 34 | print("<< " + common.color.BOLD + common.color.OKGREEN + sys.argv[ 35 | 0] + ": Kernel Address Dumper " + common.color.ENDC + ">>\n") 36 | 37 | if not self_check(): 38 | return 1 39 | 40 | from info.core import start 41 | return start() 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /mcat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (C) 2019 Sergej Schumilo, Cornelius Aschermann, Tim Blazytko 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import msgpack 21 | import os 22 | import sys 23 | from pprint import pprint 24 | 25 | sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + "/../") 26 | from common.util import read_binary_file 27 | 28 | for arg in sys.argv[1:]: 29 | pprint(msgpack.unpackb(read_binary_file(arg))) 30 | -------------------------------------------------------------------------------- /paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RUB-SysSec/grimoire/fae90ee436fe626f54ec2421269de8340cf06bf9/paper.png -------------------------------------------------------------------------------- /qemu.patch: -------------------------------------------------------------------------------- 1 | --- pt.c 2019-10-09 14:20:53.579872436 +0200 2 | +++ pt_new.c 2019-10-09 14:08:58.030661810 +0200 3 | @@ -88,7 +88,7 @@ 4 | void pt_reset_bitmap(void){ 5 | if(bitmap){ 6 | last_ip = 0ULL; 7 | - memset(bitmap, 0xff, kafl_bitmap_size); 8 | + memset(bitmap, 0x00, kafl_bitmap_size); 9 | } 10 | } --------------------------------------------------------------------------------