├── 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 | }
--------------------------------------------------------------------------------