├── INSTALL.md ├── LICENSE ├── README.md ├── cli.py ├── config.py ├── core.py ├── debug.py ├── fuzz.py ├── input ├── doc │ ├── test.doc │ └── test.docx ├── ftp │ ├── req.txt │ └── resp.txt ├── http │ ├── req.txt │ └── resp.txt ├── ssh-resp │ ├── 1 │ ├── 2 │ └── 3 └── tex │ └── test.tex ├── litefuzz.py ├── misc.py ├── mutator.py ├── net.py ├── requirements ├── requirements-py2.txt └── requirements-py3.txt ├── run.py ├── settings.py ├── setup ├── linux.sh ├── mac-xcode.sh ├── mac.sh └── windows.bat ├── test ├── a.c ├── b.c ├── c.c ├── d-gui.c ├── e.c ├── f.c ├── linux │ └── Makefile ├── mac │ └── Makefile └── windows │ └── Makefile ├── test_litefuzz.py └── triage.py /INSTALL.md: -------------------------------------------------------------------------------- 1 | In a command terminal, clone the repro and run the appropriate setup script for your OS. 2 | 3 | ``` 4 | git clone https://github.com/sec-tools/litefuzz 5 | cd litefuzz 6 | chmod +x setup/[OS].sh (linux/mac) 7 | setup/[OS][.sh|bat] 8 | ``` 9 | 10 | ### linux 11 | Make sure you run it as a user that has sudo privileges. 12 | 13 | `user@box:~$ setup/linux.sh` 14 | 15 | note: if using py2, ignore any Pyradamsa pip failures as Pyradamsa supports py3 only. 16 | 17 | ### mac 18 | Again, make sure you run it as a user that has sudo privileges. 19 | 20 | `mac:~ user$ setup/mac.sh` 21 | 22 | ### windows 23 | Open an Administrator command prompt and run the script. It will pull down a lot of packages, so it may take some time for the setup to complete. 24 | 25 | `C:\litefuzz> setup\windows.bat` 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 sec-tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # cli.py 7 | # 8 | # 9 | 10 | import sys 11 | import argparse 12 | 13 | import settings 14 | 15 | # 16 | # parse and return command line arguments 17 | # 18 | def arg_parse(): 19 | parser = argparse.ArgumentParser() 20 | 21 | parser.add_argument("-l", 22 | "--local", 23 | default=False, 24 | action="store_true", 25 | help="target will be executed locally") 26 | 27 | parser.add_argument("-k", 28 | "--client", 29 | default=False, 30 | action="store_true", 31 | help="target a network client") 32 | 33 | parser.add_argument("-s", 34 | "--server", 35 | default=False, 36 | action="store_true", 37 | help="target a network server") 38 | 39 | parser.add_argument("-c", 40 | "--cmdline", 41 | type=str, 42 | help="target command line") 43 | 44 | parser.add_argument("-i", 45 | "--inputs", 46 | type=str, 47 | help="input directory or file") 48 | 49 | parser.add_argument("-n", 50 | "--iterations", 51 | type=int, 52 | default=settings.ITERATIONS_DEFAULT, 53 | help="number of fuzzing iterations (default: %d)" % settings.ITERATIONS_DEFAULT) 54 | 55 | parser.add_argument("-x", 56 | "--maxtime", 57 | type=float, 58 | default=settings.MAX_TIME_DEFAULT, 59 | help="timeout for the run (default: %d)" % settings.MAX_TIME_DEFAULT) 60 | 61 | parser.add_argument("--mutator", 62 | "--mutator", 63 | type=int, 64 | default=settings.MUTATOR_CHOICE, 65 | help="timeout for the run (default: %d=random)" % settings.MUTATOR_CHOICE) 66 | 67 | parser.add_argument("-a", 68 | "--address", 69 | type=str, 70 | help="server address in the ip:port format") 71 | 72 | parser.add_argument("-o", 73 | "--crashdir", 74 | type=str, 75 | help="specify the directory to output crashes (default: crashes)") 76 | 77 | parser.add_argument("-t", 78 | "--tempdir", 79 | type=str, 80 | help="specify the directory to output runtime fuzzing artifacts (default: OS tmp + run dir)") 81 | 82 | parser.add_argument("-f", 83 | "--fuzzfile", 84 | type=str, 85 | help="specify the path and filename to place the fuzzed file (default: OS tmp + run dir + fuzz_random.ext)") 86 | 87 | parser.add_argument("-m", 88 | "--minfile", 89 | type=str, 90 | help="specify a crashing file to generate a minimized version of it (bonus: may also find variant bugs)") 91 | 92 | parser.add_argument("-mm", 93 | "--supermin", 94 | type=str, 95 | help="loops minimize to grind on until no more bytes can be removed") 96 | 97 | parser.add_argument("-r", 98 | "--reprofile", 99 | type=str, 100 | help="specify a crashing file or directory to replay on the target") 101 | 102 | parser.add_argument("-e", 103 | "--reuse", 104 | default=False, 105 | action="store_true", 106 | help="enable second round fuzzing where any crashes found are reused as inputs") 107 | 108 | parser.add_argument("-p", 109 | "--multibin", 110 | default=False, 111 | action="store_true", 112 | help="use multiple requests or responses as inputs for fuzzing simple binary network sessions") 113 | 114 | parser.add_argument("-pp", 115 | "--multistr", 116 | default=False, 117 | action="store_true", 118 | help="use multiple requests or responses within input for fuzzing simple string-based network sessions") 119 | 120 | parser.add_argument("--multinum", 121 | "--multinum", 122 | type=int, 123 | help="only fuzz this specific input in multi modes (eg. 2)") 124 | 125 | parser.add_argument("-u", 126 | "--insulate", 127 | default=False, 128 | action="store_true", 129 | help="only execute the target once and inside a debugger (eg. interactive clients)") 130 | 131 | parser.add_argument("--nofuzz", 132 | "--nofuzz", 133 | default=False, 134 | action="store_true", 135 | help="send input as-is without mutation (useful for debugging)") 136 | 137 | parser.add_argument("--key", 138 | "--key", 139 | type=str, 140 | help="send a particular key every iteration for interactive targets (eg. F5 for refresh)") 141 | 142 | parser.add_argument("--click", 143 | "--click", 144 | default=False, 145 | action="store_true", 146 | help="click the mouse (eg. position the cursor over target button to click beforehand)") 147 | 148 | parser.add_argument("--tls", 149 | "--tls", 150 | default=False, 151 | action="store_true", 152 | help="enable TLS for network fuzzing") 153 | 154 | parser.add_argument("--golang", 155 | "--golang", 156 | default=False, 157 | action="store_true", 158 | help="enable fuzzing of Golang binaries") 159 | 160 | parser.add_argument("--attach", 161 | "--attach", 162 | type=str, 163 | help="attach to a local server process name (mac only)") 164 | 165 | parser.add_argument("--cmd", 166 | "--cmd", 167 | type=str, 168 | help="execute this command after each fuzzing iteration (eg. umount /Volumes/test.dir)") 169 | 170 | parser.add_argument("--rmfile", 171 | "--rmfile", 172 | type=str, 173 | help="remove this file after every fuzzing iteration (eg. target won't overwrite output file)") 174 | 175 | parser.add_argument("--reportcrash", 176 | "--reportcrash", 177 | type=str, 178 | help="use ReportCrash to help catch crashes for a specified process name (mac only)") 179 | 180 | parser.add_argument("--memdump", 181 | "--memdump", 182 | default=False, 183 | action="store_true", 184 | help="enable memory dumps (win32)") 185 | 186 | parser.add_argument("--nomemdump", 187 | "--nomemdump", 188 | default=False, 189 | action="store_true", 190 | help="disable memory dumps (win32)") 191 | 192 | parser.add_argument("-z", 193 | "--malloc", 194 | type=str, 195 | nargs='?', 196 | const='default', 197 | help="enable malloc debug helpers (free bugs, but perf cost)") 198 | 199 | parser.add_argument("-zz", 200 | "--nomalloc", 201 | default=False, 202 | action="store_true", 203 | help="disable malloc debug helpers (eg. pageheap)") 204 | 205 | parser.add_argument("-d", 206 | "--debug", 207 | default=False, 208 | action="store_true", 209 | help="Turn on debug statements") 210 | 211 | args = parser.parse_args() 212 | 213 | if(len(sys.argv) == 1): 214 | parser.print_help() 215 | parser.exit() 216 | 217 | return args 218 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # config.py 7 | # 8 | # 9 | 10 | import signal 11 | 12 | # main 13 | address = None 14 | attach = None 15 | dbg32 = None 16 | debug = False 17 | cmd = None 18 | count = 0 19 | current_input = None 20 | env = None 21 | exec_avg = 0 22 | exec_times = [] 23 | fuzz = False 24 | golang = False 25 | inputs = None 26 | insulate = False 27 | insulate_pid = None 28 | key = None 29 | kill_proc = None 30 | iterations = 0 31 | maxtime = 0 32 | min = False 33 | min_current = 0 34 | min_original = 0 35 | min_pc = None 36 | min_hit = False 37 | supermin = False 38 | memdump_pid = None 39 | multibin = False 40 | multistr = False 41 | multinum = None 42 | mode = 0 43 | nofuzz = False 44 | pb = None 45 | process = None 46 | report_crash = False 47 | report_list = [] 48 | repro = False 49 | returncode = None 50 | reusedir = None 51 | rmfile = None 52 | rmtemp = True 53 | run_id = 0 54 | session = [] 55 | session_prev = [] 56 | show_stats = True 57 | static_fuzz_file = None 58 | target = None 59 | try_again = False 60 | 61 | # crash 62 | crash = False 63 | duplicate = False 64 | crashes = 0 65 | dups = 0 66 | current_pc = None 67 | pc_list = [] 68 | 69 | # network 70 | broken_pipe = False 71 | conn = None 72 | sock = None 73 | context = None 74 | cli_conn = None 75 | down = False 76 | prot = None 77 | host = None 78 | port = None 79 | replay_file = None 80 | cert = None 81 | tls = False 82 | triage = False 83 | 84 | # files 85 | crash_file = '' 86 | file_ext = '' 87 | 88 | # pause/resume 89 | org_sigint = signal.getsignal(signal.SIGINT) 90 | -------------------------------------------------------------------------------- /core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # core.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import binascii 13 | import glob 14 | import re 15 | import shutil 16 | import signal 17 | import socket 18 | import ssl 19 | import subprocess32 as subprocess 20 | import time 21 | from datetime import datetime 22 | from time import time as timer 23 | from tqdm import tqdm 24 | 25 | import run 26 | import net 27 | import triage 28 | import debug 29 | import misc 30 | import config 31 | import settings 32 | from settings import SUCCESS, FAILURE 33 | 34 | # 35 | # diff original and fuzzed files 36 | # 37 | def createDiff(original): 38 | if(config.debug): 39 | print("entering createDiff()\n") 40 | 41 | od_cmd = [] 42 | 43 | if(misc.isUnix()): 44 | preexec_fn = os.setsid 45 | else: 46 | preexec_fn = None 47 | 48 | if(misc.isUnix()): 49 | #od_cmd = 'od -w1 -vAn -tx1 ' # mac's od may not support -w flag 50 | od_cmd.append('od') 51 | else: 52 | od_cmd.append(settings.OD_WIN_BIN) 53 | 54 | od_cmd.append('-vAn') 55 | od_cmd.append('-tx1') 56 | 57 | od_cmd.append(original) 58 | 59 | if(config.debug): 60 | print("%s\n" % od_cmd) 61 | 62 | # 63 | # write od output (part 1) 64 | # 65 | try: 66 | with open(settings.FUZZ_DIFF_ORIG, 'w') as file: 67 | process = subprocess.Popen(od_cmd, 68 | stdin=None, 69 | stdout=file, 70 | stderr=None, 71 | preexec_fn=preexec_fn) 72 | 73 | (output, error) = process.communicate(timeout=settings.TOOL_TIMEOUT) 74 | 75 | if(error): 76 | print("\n[ERROR] '%s' @ pid=%d: %s\n" % (cmdline, process.pid(), error)) 77 | except subprocess.TimeoutExpired as error: 78 | if(config.debug): 79 | print("%s\n" % error) 80 | 81 | misc.killProcess(process.pid) 82 | except Exception as error: 83 | print("\n[ERROR] core.diff @ run od orig: %s\n" % error) 84 | return FAILURE 85 | 86 | od_cmd.pop() 87 | od_cmd.append(settings.FUZZ_FILE) 88 | 89 | if(config.debug): 90 | print("%s\n" % od_cmd) 91 | 92 | # 93 | # write od output (part 2) 94 | # 95 | try: 96 | with open(settings.FUZZ_DIFF_FUZZ, 'w') as file: 97 | process = subprocess.Popen(od_cmd, 98 | stdin=None, 99 | stdout=file, 100 | stderr=None, 101 | preexec_fn=preexec_fn) 102 | 103 | (output, error) = process.communicate(timeout=settings.TOOL_TIMEOUT) 104 | 105 | if(error): 106 | print("\n[ERROR] '%s' @ pid=%d: %s\n" % (cmdline, process.pid(), error)) 107 | except subprocess.TimeoutExpired as error: 108 | if(config.debug): 109 | print("%s\n" % error) 110 | 111 | misc.killProcess(process.pid) 112 | except Exception as error: 113 | print("\n[ERROR] core.diff @ run od fuzz: %s\n" % error) 114 | return FAILURE 115 | 116 | diff_cmd = [] 117 | 118 | if(misc.isUnix()): 119 | diff_cmd.append('diff') 120 | else: 121 | diff_cmd.append(settings.DIFF_WIN_BIN) 122 | 123 | # with context 124 | # diff_cmd.append('-u') 125 | # diff_cmd.append('-b') 126 | # diff_cmd.append('-B') 127 | 128 | # without context 129 | diff_cmd.append('--changed-group-format=-%<+%>') 130 | diff_cmd.append('--unchanged-group-format=') 131 | 132 | diff_cmd.append(settings.FUZZ_DIFF_ORIG) 133 | diff_cmd.append(settings.FUZZ_DIFF_FUZZ) 134 | 135 | # 136 | # write diff output 137 | # 138 | try: 139 | with open(settings.FUZZ_DIFF, 'w') as file: 140 | process = subprocess.Popen(diff_cmd, 141 | stdin=None, 142 | stdout=file, 143 | stderr=None, 144 | preexec_fn=preexec_fn) 145 | (output, error) = process.communicate(timeout=settings.TOOL_TIMEOUT) 146 | 147 | if(error): 148 | print("\n[ERROR] '%s' @ pid=%d: %s\n" % (cmdline, process.pid(), error)) 149 | except subprocess.TimeoutExpired as error: 150 | if(config.debug): 151 | print("%s\n" % error) 152 | 153 | misc.killProcess(process.pid) 154 | except Exception as error: 155 | print("\n[ERROR] core.diff @ run diff: %s\n" % error) 156 | return FAILURE 157 | 158 | # 159 | # try and create an analogous strings diff 160 | # 161 | if(createDiffString() != SUCCESS): 162 | return FAILURE 163 | 164 | return SUCCESS 165 | 166 | # 167 | # take the default binary diff file and produce a strings version 168 | # 169 | def createDiffString(): 170 | if(config.debug): 171 | print("entering createDiffString()\n") 172 | 173 | try: 174 | with open(settings.FUZZ_DIFF, 'r') as file: 175 | bdiff = file.readlines() 176 | except Exception as error: 177 | print("\n[ERROR] core.createDiffString() @ read(FUZZ_DIFF): %s\n" % error) 178 | return FAILURE 179 | 180 | sdiff = [] 181 | 182 | for line in bdiff: 183 | if(re.match('^(\s|\-|\+)(\s)', line)): 184 | sdiff.append(line) 185 | 186 | diff = [] 187 | 188 | for line in sdiff: 189 | chars = line.split(' ') 190 | 191 | for (i, char) in enumerate(chars): 192 | if((char != '') and (re.match('(\s)(\-|\+)(\s)', char) == None)): 193 | try: 194 | if(misc.isMac() or misc.isWin32()): # es especial 195 | chars[i] = str(binascii.unhexlify(char)) 196 | else: 197 | chars[i] = str(bytes.fromhex(char).decode('ascii')) 198 | except UnicodeDecodeError: 199 | chars[i] = str('\\x' + chars[i].upper()) 200 | pass 201 | except Exception as error: 202 | if(config.debug): 203 | print("[INFO] core.createDiffString() exception: %s\n" % error) 204 | continue 205 | 206 | # 207 | # add newline at the end 208 | # 209 | if(i == (len(chars) - 1)): 210 | chars[i] = chars[i] + '\n' 211 | 212 | diff.append(''.join(chars)) 213 | 214 | diff = ''.join(diff) 215 | 216 | # 217 | # fix formatting 218 | # 219 | diffs = diff.splitlines() 220 | 221 | diff = '' 222 | 223 | for line in diffs: 224 | if(line.startswith('+')): 225 | line = line.replace('+', '+ ') 226 | 227 | if(line.startswith('-')): 228 | line = line.replace('-', '- ') 229 | 230 | diff += line + '\n' 231 | 232 | # 233 | # write diff strings edition (tm) 234 | # 235 | try: 236 | with open(settings.FUZZ_DIFF_STRING, 'w') as file: 237 | file.write(diff) 238 | except Exception as error: 239 | print("\n[ERROR] core.createDiffString() @ write(FUZZ_DIFF_STRING): %s\n" % error) 240 | return FAILURE 241 | 242 | return SUCCESS 243 | 244 | # 245 | # poor man's version of crash minimization (no external deps) 246 | # 247 | # idea: starting an index 0, remove each byte and check if it crashes 248 | # 249 | # - if it still crashes, keep the mutant and continue to the next byte 250 | # - if it doesn't crash, restore the byte and continue to the next byte 251 | # 252 | # note: we're on with the target crashing on a different PC as long as it still 253 | # crashes and keep all additional crashes as artifacts 254 | # 255 | # other notes 256 | # 257 | # - reuse the run dir and crash dir for all this 258 | # - this type of minimization can act as a type of focused-fuzzing itself 259 | # -- where you take an initial crash and you're minimizing it, but may also get more/new crashes 260 | # 261 | # 262 | def minimize(file, cmdline, malloc, address): 263 | if(config.debug): 264 | print("entering minimize()") 265 | 266 | if(config.multibin or config.multistr): 267 | print("[ERROR] minimization isn't supported for session fuzzing\n") 268 | sys.exit(FAILURE) 269 | 270 | # 271 | # disable artifacts temporarily for crash repro and update crash dir 272 | # 273 | settings.ARTIFACTS_ENABLE = False 274 | 275 | if(file != config.current_input): # don't change crash dir for supermin mode 276 | settings.CRASH_DIR = settings.CRASH_DIR + '-min' 277 | 278 | if(config.insulate or (config.mode == settings.LOCAL_SERVER)): 279 | config.try_again = True 280 | 281 | # 282 | # only for local/client mode 283 | # 284 | if(misc.preChecks() == False): 285 | return FAILURE 286 | 287 | # 288 | # setup command line 289 | # 290 | org_cmdline = cmdline 291 | cmdline = misc.setupCmdline(cmdline) 292 | 293 | if(cmdline == None and (config.attach == None)): 294 | return FAILURE 295 | 296 | if(config.show_stats): 297 | misc.displayStats(org_cmdline, None, None, file) 298 | 299 | if(config.multibin): 300 | if(os.path.isdir(file) == False): 301 | print("[ERROR] %s is not a directory (multibin requires a directory of session files)\n" % file) 302 | return FAILURE 303 | else: 304 | if(os.path.isfile(file) == False): 305 | print("[ERROR] %s is not a file\n" % file) 306 | return FAILURE 307 | 308 | if(os.path.getsize(file) == 0): 309 | print("\n[ERROR] core.minimize() @ chosen input: %s is empty\n" % os.path.basename(file)) 310 | return FAILURE 311 | 312 | if(reproCrash(file, cmdline, True) != SUCCESS): 313 | return FAILURE 314 | 315 | if(config.show_stats): 316 | print("[+] repro OK\n") 317 | print("[+] starting minimization\n") 318 | 319 | settings.ARTIFACTS_ENABLE = True # turn this back on to get artifacts for any new crashes 320 | 321 | # 322 | # "we are not yet in supermin mode" so save the original min filename to restore it as the final later 323 | # 324 | if(config.show_stats): 325 | settings.MIN_FILE_ORIG = settings.MIN_FILE 326 | 327 | # 328 | # swap the fuzz file used to repro the crash for the min file 329 | # 330 | if((config.mode != settings.CLIENT) and (config.mode != settings.SERVER)): 331 | for i, cmd in enumerate(cmdline): 332 | if(cmd == settings.FUZZ_FILE): 333 | cmdline[i] = settings.MIN_FILE 334 | 335 | settings.FUZZ_FILE = settings.MIN_FILE # for diffs from now on 336 | 337 | # 338 | # reset counters and cleanup files after test crash run 339 | # 340 | if(config.show_stats): # don't remove previous min's in supermin mode 341 | misc.cleanupMin() 342 | 343 | misc.resetCounters() 344 | 345 | data = misc.readBytes(file) 346 | 347 | if(data == None): 348 | return FAILURE 349 | 350 | min_temp = bytearray(len(data)) 351 | min_data = bytearray() 352 | 353 | min_temp[:] = data 354 | 355 | config.min_original = len(data) 356 | 357 | pb = tqdm(total=0, bar_format="{desc}") 358 | 359 | if(config.debug): 360 | print("\n%s\n" % min_temp) 361 | 362 | # 363 | # starting at the first index, remove each byte 364 | # 365 | for i, b in enumerate(min_temp): 366 | if(config.debug): 367 | print("byte %d/%d -> %d\n" % (i, (len(data) - 1), b)) 368 | 369 | # 370 | # make a copy of min_temp 371 | # 372 | min_data[:] = min_temp 373 | 374 | config.min_current = len(min_temp) 375 | 376 | # 377 | # remove byte at index 378 | # 379 | min_temp.pop(i) 380 | 381 | if(config.debug): 382 | print("\n%s\n" % min_temp) 383 | 384 | misc.writeBytes(settings.MIN_FILE, min_temp) 385 | 386 | if(config.debug): 387 | print("\nsettings.MIN_FILE len = %d\n" % len(min_temp)) 388 | 389 | # 390 | # local app and client modes 391 | # 392 | if((config.mode == settings.LOCAL) or (config.mode == settings.LOCAL_CLIENT)): 393 | run.main(cmdline) 394 | # 395 | # local server mode 396 | # 397 | elif(config.mode == settings.LOCAL_SERVER): 398 | config.replay_file = settings.MIN_FILE 399 | 400 | net.replay(cmdline) 401 | 402 | else: 403 | print("[ERROR] minimization for remote targets is unsupported\n") 404 | return FAILURE 405 | 406 | # 407 | # helps to fuzz interactive client GUIs such 408 | # as filezilla, maybe even web browsers, etc 409 | # 410 | if(config.key != None): 411 | misc.hitKey() 412 | 413 | misc.copyDebugOutput() 414 | 415 | # 416 | # check for crashes from debugger output 417 | # 418 | if((config.insulate) or (config.mode == settings.LOCAL_SERVER)): 419 | if(misc.isUnix()): 420 | triage.checkDebugger(cmdline) 421 | 422 | config.count = i # for min, we use it for current index 423 | config.iterations += 1 424 | 425 | # 426 | # show progress 427 | # 428 | if(i == (len(min_temp) - 1)): 429 | misc.displayCount(pb, len(min_temp), False) # hm, strange but works 430 | else: 431 | misc.displayCount(pb, (len(min_temp) + 1), False) # since we pop'd one off, account for that 432 | 433 | if(config.debug): 434 | print("\nmin_temp=%d, min_data=%d\n" % (len(min_temp), len(min_data))) 435 | 436 | min_temp = minCrashAnalysis(min_temp, min_data, i, 1) 437 | 438 | config.crash = False 439 | config.duplicate = False 440 | 441 | misc.writeBytes(settings.MIN_FILE, min_data) 442 | 443 | if(config.debug): 444 | print("\nwrote min to %s\n" % settings.MIN_FILE) 445 | 446 | pb.close() 447 | 448 | if(len(min_data) == 0): 449 | print("\n[-] minimization reduced crasher to zero bytes, something probably went wrong") 450 | elif(len(min_data) == len(data)): 451 | if(config.supermin and (settings.MIN_FILE != settings.MIN_FILE_ORIG)): # we went past the first round 452 | print("\n[+] achieved maximum minimization @ %d bytes (%s)" % (len(min_data), os.path.basename(settings.MIN_FILE_ORIG))) 453 | else: 454 | print("\n[~] minimization did not reduce crasher file, perhaps the target really needs all these bytes...") 455 | 456 | config.min_hit = True 457 | 458 | if(config.supermin): 459 | # 460 | # move final min file to original min filename and remove previous min files 461 | # 462 | try: 463 | shutil.move(os.path.abspath(settings.MIN_FILE), os.path.abspath(settings.MIN_FILE_ORIG)) 464 | except Exception as error: 465 | print("\n[ERROR] core.minimize() @ supermin final move: %s\n" % error) 466 | return FAILURE 467 | 468 | for name in os.listdir(settings.CRASH_DIR): 469 | if(('.min' in name) and (name not in os.path.basename(settings.MIN_FILE_ORIG))): 470 | try: 471 | os.remove(settings.CRASH_DIR + os.sep + name) 472 | except Exception as error: 473 | print("\n[ERROR] couldn't remove other min files: %s\n" % error) 474 | return FAILURE 475 | 476 | settings.MIN_FILE = settings.MIN_FILE_ORIG 477 | settings.FUZZ_FILE = settings.MIN_FILE_ORIG # repro 478 | else: 479 | if(config.min_pc == config.current_pc): # last pc, the one that min file crashes 480 | if(config.supermin): # don't show min file iteration filenames in supermin mode 481 | print("\n[+] reduced crash @ pc=%s to %d bytes\n" % (config.min_pc, len(min_data))) 482 | else: 483 | print("\n[+] reduced crash @ pc=%s to %d bytes (%s)" % (config.min_pc, 484 | len(min_data), 485 | os.path.basename(settings.MIN_FILE))) 486 | else: 487 | if(config.supermin): 488 | print("\n[+] reduced crash @ pc=%s -> pc=%s to %d bytes\n" % (config.min_pc, 489 | config.current_pc, 490 | len(min_data))) 491 | else: 492 | print("\n[+] reduced crash @ pc=%s -> pc=%s to %d bytes (%s)" % (config.min_pc, 493 | config.current_pc, 494 | len(min_data), 495 | os.path.basename(settings.MIN_FILE))) 496 | 497 | config.min_pc = config.current_pc # now make the current pc the new min pc 498 | 499 | if(config.debug): 500 | print("\n[INFO] replaying min file for crash artifacts") 501 | 502 | if(reproCrash(settings.MIN_FILE, cmdline, False) != SUCCESS): 503 | return FAILURE 504 | 505 | return SUCCESS 506 | 507 | # 508 | # check minimized crash 509 | # 510 | # if no crash, restore copy with orginial byte 511 | # 512 | # if crash, check if its the same PC as crash during repro 513 | # -> if so, don't restore byte 514 | # -> if not, restore byte because its a new crash (we're trying to minimize based on PC) 515 | # 516 | def minCrashAnalysis(min_temp, min_data, i, mode): 517 | if((config.crash == False) and (config.duplicate == False)): 518 | if(config.debug): 519 | if(mode == 1): 520 | print("\n[INFO] no crash after removing byte=%d @ index=%d, treating byte as importante\n" % (min_data[i], i)) 521 | 522 | if(mode == 2): 523 | print("\n[INFO] no crash after removing bytes=(%d, %d) @ indices=(%d, %d), treating bytes as importante\n" % (min_data[i], 524 | min_data[i + 1], 525 | i, 526 | (i + 1))) 527 | 528 | if(mode == 4): 529 | print("\n[INFO] no crash after removing bytes=(%d, %d, %d, %d) @ indices=(%d, %d, %d, %d), treating bytes as importante\n" % (min_data[i], 530 | min_data[i + 1], 531 | min_data[i + 2], 532 | min_data[i + 3], 533 | i, 534 | (i + 1), 535 | (i + 2), 536 | (i + 3))) 537 | 538 | min_temp[:] = min_data 539 | else: 540 | if(config.debug): 541 | print("\ncurrent_pc=%s, min_pc=%s\n" % (config.current_pc, config.min_pc)) 542 | 543 | # 544 | # handle a few conditions here 545 | # 546 | # - current_pc = min_pc (crash pc), remove the byte to minimize 547 | # - current_pc is similar to min_pc, then they may be the same crash at different PCs 548 | # - current_pc = wild and min_pc = wild, then they are likely the same crash at different wild PCs 549 | # -- eg. jmp rdx and rdx is some crazy big value like 0x7755ABCD19192020 550 | # - neither of those, this is a new crash and therefore keep the byte and don't minimize for it 551 | # 552 | if(config.current_pc == config.min_pc): 553 | if(config.debug): 554 | print("\n[INFO] target still crashed with PC=%s after removing byte=%d @ index=%d\n" % (config.current_pc, min_data[i], i)) 555 | elif(misc.isSimilar(config.min_pc, config.current_pc)): 556 | if(config.debug): 557 | print("\n[INFO] target crashed with new PC=%s vs original=%s, assuming same crash after removing byte=%d @ index=%d\n" % (config.current_pc, config.min_pc, min_data[i], i)) 558 | elif(misc.isWild(config.current_pc) and misc.isWild(config.min_pc)): 559 | if(config.debug): 560 | print("\n[INFO] target crashed with new wild PC=%s vs wild original=%s, assuming same crash after removing byte=%d @ index=%d\n" % (config.current_pc, config.min_pc, min_data[i], i)) 561 | elif((config.current_pc == 'UNKNOWN') or (config.min_pc == 'UNKNOWN')): 562 | if(config.debug): 563 | print("\n[INFO] target crashed but either PC=UNKNOWN or original PC=UNKNOWN, minimization may not work properly") 564 | else: 565 | if(config.debug): 566 | print("\n[INFO] target crashed with new PC=%s after removing byte=%d @ index=%d, byte will be restored\n" % (config.current_pc, min_data[i], i)) 567 | 568 | min_temp[:] = min_data 569 | 570 | return min_temp 571 | 572 | # 573 | # replay a crashing file against a target 574 | # 575 | def repro(file, cmdline, malloc, address): 576 | if(config.debug): 577 | print("entering repro()") 578 | 579 | if(config.insulate): 580 | print("[ERROR] repro mode for insulated apps is not supported\n") 581 | return FAILURE 582 | 583 | # 584 | # slight tweak 585 | # 586 | settings.DIFF_ENABLE = False 587 | 588 | if any([config.insulate or (config.mode == settings.SERVER) or (config.mode == settings.LOCAL_SERVER)]): 589 | config.try_again = True 590 | 591 | # 592 | # only for local mode 593 | # 594 | if(cmdline != None): 595 | if(misc.preChecks() == False): 596 | return FAILURE 597 | 598 | # 599 | # setup command line 600 | # 601 | if(cmdline != None): 602 | org_cmdline = cmdline 603 | cmdline = misc.setupCmdline(cmdline) 604 | else: 605 | org_cmdline = None 606 | 607 | if(config.show_stats): 608 | misc.displayStats(org_cmdline, None, None, file) 609 | 610 | if(config.multibin): 611 | if(os.path.isdir(file) == False): 612 | print("[ERROR] %s is not a directory (multibin requires a directory of session files)\n" % file) 613 | return FAILURE 614 | else: 615 | if(os.path.isfile(file) == False): 616 | print("[ERROR] %s is not a file\n" % file) 617 | return FAILURE 618 | 619 | if(os.path.getsize(file) == 0): 620 | print("\n[ERROR] core.repro() @ chosen input: %s is empty\n" % os.path.basename(file)) 621 | return FAILURE 622 | 623 | if(reproCrash(file, cmdline, True) != SUCCESS): 624 | return FAILURE 625 | 626 | if(config.repro): 627 | if(config.dups > 0): 628 | config.crash = True 629 | 630 | if(config.repro): 631 | config.dups = config.crashes 632 | 633 | return SUCCESS 634 | 635 | # 636 | # core repro 637 | # 638 | def reproCrash(file, cmdline, banner): 639 | if(config.debug): 640 | print("entering reproCrash()\n") 641 | 642 | if(config.insulate): 643 | if(config.debug): 644 | print("[INFO] repro for insulated targets is unsupported\n") 645 | return FAILURE 646 | 647 | if(banner and config.show_stats): 648 | print("[+] attempting to repro the crash...") 649 | 650 | # 651 | # set it up for minimization or repro runs 652 | # 653 | if(config.min or config.repro): 654 | config.current_input = os.path.abspath(file) 655 | 656 | files = None 657 | 658 | if(config.multibin): 659 | try: 660 | files = glob.glob(file + '/*') # file is actually a repro directory here 661 | except Exception as error: 662 | print("[ERROR] core.reproCrash() @ glob: %s\n" % error) 663 | return FAILURE 664 | 665 | data = None 666 | 667 | if(config.multistr): 668 | data = misc.readBytes(file) 669 | 670 | if(data == None): 671 | print("[ERROR] core.reproCrash() @ reading the repro '%s': failed\n" % file) 672 | return FAILURE 673 | 674 | if(config.multibin or config.multistr): 675 | if(misc.setupSession(files, data) != SUCCESS): 676 | return FAILURE 677 | 678 | if(misc.setupNewIteration(cmdline) != SUCCESS): 679 | return FAILURE 680 | 681 | # 682 | # don't setup new iteration during repro for debugger-ran apps 683 | # 684 | if((config.insulate == False) and (config.mode != settings.LOCAL_SERVER)): 685 | if(misc.setupNewIteration(cmdline) != SUCCESS): 686 | return FAILURE 687 | 688 | # 689 | # don't increment count here during minimization 690 | # 691 | if(config.repro): 692 | config.count += 1 693 | 694 | # 695 | # fix the crash repro file for supermin mode 696 | # 697 | if(config.supermin and config.min_hit): 698 | for (i, cmd) in enumerate(cmdline): 699 | if('.min' in cmd): 700 | cmdline[i] = file 701 | 702 | if(config.mode == settings.LOCAL): 703 | if(config.debug): 704 | print("\nreproing a local app target\n") 705 | 706 | if(config.insulate): 707 | if(config.debug): 708 | print("\n[INFO] reproing crashes with insulated apps is experimental and has not been tested") 709 | 710 | if(debug.main(cmdline) != SUCCESS): 711 | print("\n[ERROR] failed to run cmdline in debugger\n") 712 | return FAILURE 713 | 714 | misc.doInsulate() 715 | else: 716 | if(run.main(cmdline) != SUCCESS): 717 | print("\n[ERROR] failed to run cmdline\n") 718 | return FAILURE 719 | 720 | if((cmdline != None) and (config.process != None)): 721 | misc.checkForCrash(cmdline) 722 | else: 723 | if(config.debug): 724 | print("\nreproing a local or remote network target\n") 725 | 726 | if(config.address != None): 727 | misc.setupAddress() 728 | 729 | if(None in (config.prot, config.host, config.port)): 730 | if(config.debug): 731 | print("\n[ERROR] target address should be in proto://host:port format: %s\n" % config.address) 732 | return FAILURE 733 | 734 | if((misc.checkIPAddress(config.host) == False) and (misc.checkHostname(config.host) == False)): 735 | if(config.debug): 736 | print("\n[ERROR] invalid ip or hostname: %s\n" % config.host) 737 | return FAILURE 738 | 739 | if(config.mode == settings.SERVER): 740 | if(misc.checkPort(config.prot, config.host, config.port) == False): 741 | if(config.repro): 742 | print("\n[ERROR] core.reproCrash @ connection failed to %s\n" % config.address) 743 | 744 | return FAILURE 745 | 746 | if(config.insulate): 747 | debug.main(cmdline) 748 | 749 | misc.doInsulate() 750 | else: 751 | config.replay_file = file 752 | 753 | net.replay(cmdline) 754 | 755 | # 756 | # helps to fuzz interactive client GUIs such 757 | # as filezilla, maybe even web browsers, etc 758 | # 759 | if(config.key != None): 760 | misc.hitKey() 761 | 762 | # 763 | # only copy debug output during fuzzing 764 | # 765 | if((config.min == False) and (config.repro == False)): 766 | if(misc.copyDebugOutput() != SUCCESS): 767 | print("\n[ERROR] core.reproCrash() @ copyDebugOutput() failed\n") 768 | 769 | # 770 | # check for crashes from debugger output 771 | # 772 | if((config.insulate) or (config.mode == settings.LOCAL_SERVER)): 773 | if(misc.isUnix()): 774 | if(triage.checkDebugger(cmdline) != SUCCESS): 775 | if(config.debug): 776 | print("\n[INFO] core.repro() no crash in debugger\n") 777 | 778 | # 779 | # during local app runs, config.crash gets set during triage 780 | # 781 | # note: for insulated or local server apps, we check them twice just in case of timing issues 782 | # 783 | if((config.mode == settings.CLIENT) or (config.mode == settings.SERVER)): 784 | if(config.repro): 785 | if(config.mode == settings.CLIENT): 786 | print("OK, check to see if remote client crashed\n") 787 | else: 788 | if(misc.checkPort(config.prot, config.host, config.port) == False): 789 | misc.newCrash() 790 | elif(config.attach != None): 791 | if(misc.checkReportCrash()): 792 | misc.newCrash() 793 | else: 794 | print("\n[!] target server still seems to be up") 795 | else: 796 | config.replay_file = file 797 | 798 | if(config.mode == settings.CLIENT): 799 | if(net.replay(cmdline) != SUCCESS): 800 | misc.newCrash() 801 | 802 | # 803 | # stop fuzzing only if we're not doing any local/remote hybrid stuff 804 | # 805 | if((config.attach == None) and (config.report_crash == False)): 806 | config.down = True 807 | 808 | if(config.multibin): 809 | repro = None # falls back to using config.session in saveRepro() 810 | else: 811 | repro = file 812 | 813 | return triage.saveRepro(repro) 814 | 815 | # 816 | # if the full replay process fails, double check by trying to connect to the server 817 | # 818 | if(config.mode == settings.SERVER): 819 | if(net.replay(cmdline) != SUCCESS): 820 | if(misc.checkPort(config.prot, config.host, config.port) == False): 821 | misc.newCrash() 822 | 823 | if((config.attach == None) and (config.report_crash == False)): 824 | config.down = True 825 | 826 | if(config.multibin): 827 | repro = None # falls back to using config.session in saveRepro() 828 | else: 829 | repro = file 830 | 831 | return triage.saveRepro(file) 832 | else: 833 | if((config.crash == False) and (config.duplicate == False)): 834 | if(config.insulate or (config.mode == settings.LOCAL_SERVER)): 835 | if(config.try_again): 836 | config.try_again = False 837 | 838 | if(config.debug): 839 | print("target didn't crash, waiting for %f seconds and trying again...\n" % (config.maxtime / 2)) 840 | 841 | time.sleep((config.maxtime / 2)) 842 | 843 | reproCrash(file, cmdline, False) 844 | else: 845 | print("\n[ERROR] target did not crash -- check cmdline, minfile and malloc debug options OR increase maxtime\n") 846 | # return FAILURE 847 | sys.exit(FAILURE) 848 | else: 849 | if(config.debug): 850 | if(len(config.pc_list) > 0): 851 | print("\n[INFO] target crashed @ pc=%s\n" % config.current_pc) 852 | else: 853 | print("\n[INFO] target crashed (dup)\n") 854 | 855 | return SUCCESS 856 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # debug.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import glob 13 | import shutil 14 | import subprocess32 as subprocess 15 | import time 16 | 17 | if(str(sys.platform).startswith('win32')): 18 | from winappdbg import * 19 | 20 | import core 21 | import triage 22 | import misc 23 | import config 24 | import settings 25 | from settings import SUCCESS, FAILURE 26 | 27 | # 28 | # choose your own adventure 2 29 | # 30 | def main(cmdline): 31 | if(misc.isLinux()): 32 | return gdb(cmdline) 33 | elif(misc.isMac()): 34 | return lldb(cmdline) 35 | else: # winappdbg already handles this for win32 36 | return FAILURE 37 | 38 | # 39 | # Linux crash triage with the gdb debugger 40 | # 41 | def gdb(cmdline): 42 | if(config.debug): 43 | print("entering debug.gdb()\n") 44 | 45 | target = cmdline[0] 46 | args = cmdline[1:] 47 | 48 | debug_timeout = (config.maxtime * settings.DEBUG_TIMEOUT_MULTIPLE) 49 | 50 | # 51 | # if we only have a relative name for the target, get the full path for gdb 52 | # 53 | if(os.sep not in target): 54 | target = misc.getExePath(target) 55 | 56 | if(config.debug): 57 | print("target=%s, args=%s\n" % (target, args)) 58 | 59 | debug_cmdline = [] 60 | 61 | debug_cmdline.append('gdb') 62 | debug_cmdline.append('-q') 63 | 64 | debug_cmdline.append('-ex') 65 | debug_cmdline.append('file "' + target + '"') 66 | 67 | if(settings.DEBUG_ENV_EFENCE_GLIBC_ENABLE): 68 | debug_cmdline.append('-ex') 69 | 70 | # 71 | # set environment LD_PRELOAD /usr/lib/libefence.so 72 | # r ... 73 | # 74 | if(settings.DEBUG_ENV_EFENCE_ENABLE): 75 | debug_cmdline.append('set environment LD_PRELOAD ' + settings.LIBEFENCE_PATH) 76 | else: 77 | debug_cmdline.append('set environment ' + settings.GLIBC_MALLOC_CHECK) # fallback 78 | 79 | debug_cmdline.append('-ex') 80 | debug_cmdline.append('shell echo') 81 | 82 | debug_cmdline.append('-ex') 83 | 84 | run_cmdline = 'run ' 85 | 86 | if(args != None): 87 | for arg in args: 88 | run_cmdline += arg + ' ' 89 | 90 | run_cmdline = run_cmdline[:-1] 91 | 92 | debug_cmdline.append(run_cmdline) 93 | 94 | debug_cmdline.append('-ex') 95 | debug_cmdline.append('bt') 96 | 97 | debug_cmdline.append('-ex') 98 | debug_cmdline.append('shell echo') 99 | 100 | debug_cmdline.append('-ex') 101 | debug_cmdline.append('info registers') 102 | 103 | if(config.golang): 104 | debug_cmdline.append('-ex') 105 | debug_cmdline.append('disas $pc') 106 | else: 107 | # bang exploitable already provides a short disassembly, but it can fail 108 | debug_cmdline.append('-ex') 109 | debug_cmdline.append('x/4i $pc') 110 | 111 | debug_cmdline.append('-ex') 112 | debug_cmdline.append('exploitable -v') 113 | 114 | debug_cmdline.append('-ex') 115 | debug_cmdline.append('set confirm off') 116 | debug_cmdline.append('-ex') 117 | debug_cmdline.append('quit') 118 | 119 | if(config.debug): 120 | print("%s" % debug_cmdline) 121 | 122 | if(run(debug_cmdline, debug_timeout) == FAILURE): 123 | if(config.debug): 124 | print("debug.gdb() calling debug.run() failure\n") 125 | sys.exit(FAILURE) 126 | 127 | return SUCCESS 128 | 129 | # 130 | # Mac crash triage with the lldb debugger 131 | # 132 | # reference some stuff in https://github.com/bnagy/francis/blob/master/exploitaben/exploitaben.py 133 | # it would be nice if one could get exploitaben working for !exploitable-like triage 134 | # 135 | def lldb(cmdline): 136 | if(config.debug): 137 | print("entering debug.lldb()\n") 138 | 139 | target = cmdline[0] 140 | args = cmdline[1:] 141 | 142 | debug_timeout = (config.maxtime * settings.DEBUG_TIMEOUT_MULTIPLE) 143 | 144 | if(config.debug): 145 | print("target=%s, args=%s\n" % (target, args)) 146 | 147 | debug_cmdline = [] 148 | 149 | debug_cmdline.append('lldb') 150 | 151 | debug_cmdline.append('-o') 152 | debug_cmdline.append('target create "' + cmdline[0] + '"') 153 | 154 | if(settings.DEBUG_ENV_GMALLOC_ENABLE): 155 | debug_cmdline.append('-o') 156 | debug_cmdline.append('settings set target.env-vars DYLD_INSERT_LIBRARIES=' + settings.LIBGMALLOC_PATH) 157 | else: 158 | if(settings.DEBUG_ENV_GLIBC_ENABLE): 159 | debug_cmdline.append('-o') 160 | debug_cmdline.append('settings set target.env-vars ' + settings.GLIBC_MALLOC_CHECK) # fallback 161 | 162 | debug_cmdline.append('-o') 163 | 164 | run_cmdline = 'run ' 165 | 166 | if(args != None): 167 | for arg in args: 168 | run_cmdline += arg + ' ' 169 | 170 | run_cmdline = run_cmdline[:-1] 171 | 172 | debug_cmdline.append(run_cmdline) 173 | 174 | debug_cmdline.append('-o') 175 | # debug_cmdline.append('bt 24') 176 | debug_cmdline.append('bt') 177 | 178 | debug_cmdline.append('-o') 179 | debug_cmdline.append('reg read') 180 | 181 | debug_cmdline.append('-o') 182 | debug_cmdline.append(settings.DEBUG_DISSASSEMBLE_LLDB) 183 | 184 | debug_cmdline.append('-o') 185 | debug_cmdline.append('quit') 186 | 187 | if(config.debug): 188 | print("\ndebug_cmdline: %s\n" % debug_cmdline) 189 | 190 | if(run(debug_cmdline, debug_timeout) == FAILURE): 191 | if(config.debug): 192 | print("debug.lldb() calling debug.run() failure\n") 193 | 194 | sys.exit(FAILURE) 195 | 196 | return SUCCESS 197 | 198 | # 199 | # debugger execution 200 | # 201 | def run(cmdline, timeout): 202 | if(config.debug): 203 | print("\nentering debug.run()\n") 204 | print("writing debug log to %s\n" % settings.FUZZ_INFO) 205 | 206 | if(settings.KILL_EXISTING_PROCESS): 207 | if(config.debug): 208 | print("killing any running processes named %s before running a new one\n" % config.target) 209 | 210 | misc.killProcessByName(config.target) 211 | 212 | # 213 | # try and prevent the new server from not starting because the port is still occupied 214 | # 215 | if(config.mode == settings.LOCAL_SERVER): 216 | if(misc.checkPort(config.prot, 'localhost', config.port)): 217 | if(config.process != None): 218 | misc.killProcess(config.process.pid) 219 | misc.killProcessByName(config.target) 220 | 221 | try: 222 | with open(settings.FUZZ_INFO, 'w') as file: 223 | if(settings.FUZZ_FILE in cmdline): 224 | process = subprocess.Popen(cmdline, 225 | stdin=None, 226 | stdout=file, 227 | stderr=file, 228 | preexec_fn=os.setsid) 229 | else: 230 | if(config.mode == settings.LOCAL): # don't do this for local clients/servers 231 | process = subprocess.Popen(cmdline, 232 | stdin=open(config.current_input), 233 | stdout=file, 234 | stderr=file, 235 | preexec_fn=os.setsid) 236 | else: 237 | process = subprocess.Popen(cmdline, 238 | stdin=None, 239 | stdout=file, 240 | stderr=file, 241 | preexec_fn=os.setsid) 242 | 243 | if(config.mode == settings.LOCAL): 244 | (output, error) = process.communicate(timeout=timeout) 245 | 246 | if(error): 247 | print("[ERROR] '%s' @ pid=%d: %s\n" % (cmdline, process.pid(), error)) 248 | else: 249 | time.sleep(config.maxtime) # works, but no visibility into crashes 250 | except subprocess.TimeoutExpired as error: 251 | if(config.debug): 252 | print("%s\n" % error) 253 | 254 | misc.killProcess(process.pid) 255 | except Exception as error: 256 | print("[ERROR] debug.run() failed: %s\n" % error) 257 | 258 | try: 259 | file.write("debugger run failed: %s" % error) 260 | except Exception as error: 261 | print("[ERROR] debug.run() failed @ writing about it's failure: %s\n" % error) 262 | return FAILURE 263 | 264 | return FAILURE 265 | 266 | config.process = process 267 | 268 | if(config.insulate): 269 | config.insulate_pid = process.pid 270 | 271 | # 272 | # set this here to make sure we're current on the latest debugger runs 273 | # 274 | settings.FUZZ_INFO_STATIC = settings.FUZZ_INFO 275 | 276 | if(config.debug): 277 | print("debug.run() %s started @ pid=%d\n" % (cmdline, process.pid)) 278 | 279 | return SUCCESS 280 | 281 | # 282 | # attach to a process 283 | # 284 | # Reference: https://opensource.apple.com/source/lldb/lldb-159/docs/lldb-for-gdb-users.txt.auto.html 285 | # 286 | def attach(debugger, pid): 287 | if(config.debug): 288 | print("entering debug.attach()\n") 289 | 290 | settings.KILL_EXISTING_PROCESS = False 291 | 292 | debug_timeout = 9999999 # don't... just don't 293 | 294 | debug_cmdline = [] 295 | 296 | if(config.debug): 297 | print("debugger=%s and attaching to pid=%d\n" % (debugger, pid)) 298 | 299 | if(debugger == 'gdb'): 300 | print("gdb support is unimplemented\n") 301 | return FAILURE 302 | 303 | if(debugger == 'lldb'): 304 | debug_cmdline.append('sudo') # usually needed for attaching 305 | debug_cmdline.append('lldb') 306 | 307 | debug_cmdline.append('-o') 308 | debug_cmdline.append('attach -p ' + str(pid)) # also attach --name NAME 309 | 310 | debug_cmdline.append('-o') 311 | debug_cmdline.append('continue') 312 | 313 | debug_cmdline.append('-o') 314 | debug_cmdline.append('bt') 315 | 316 | debug_cmdline.append('-o') 317 | debug_cmdline.append('reg read') 318 | 319 | # 320 | # command script import lldb.macosx.heap 321 | # malloc_info -S $rax 322 | # 323 | 324 | debug_cmdline.append('-o') 325 | debug_cmdline.append(settings.DEBUG_DISSASSEMBLE_LLDB) 326 | 327 | debug_cmdline.append('-o') 328 | debug_cmdline.append('detach') 329 | 330 | debug_cmdline.append('-o') 331 | debug_cmdline.append('quit') 332 | 333 | if(config.debug): 334 | print("\ndebug_cmdline: %s\n" % debug_cmdline) 335 | 336 | if(run(debug_cmdline, debug_timeout) == FAILURE): 337 | if(config.debug): 338 | print("debug.attach() calling debug.run() failure\n") 339 | 340 | sys.exit(FAILURE) 341 | 342 | return SUCCESS 343 | 344 | # 345 | # Windows console debugger crash triage with memory dumps via !analyze 346 | # 347 | # (winappdbg already has some !exploitable type analysis) 348 | # 349 | def win32Analyze(): 350 | if(config.debug): 351 | print("entering debug.win32Analyze()\n") 352 | 353 | time.sleep(5) # delay to make sure WER has time to generate the dump file 354 | 355 | crash_dir = os.path.abspath(os.getcwd()) + os.sep + settings.CRASH_DIR 356 | 357 | hash = misc.getHash(misc.readBytes(settings.FUZZ_FILE)) 358 | 359 | if(hash == None): 360 | hash = 'UNKNOWN' 361 | 362 | # 363 | # get pid and create paths to both generated dmp file and it's final location 364 | # 365 | dump_file_orig = settings.TMP_DIR + \ 366 | os.sep + \ 367 | misc.addExtExe(os.path.basename(config.target)) + \ 368 | '.' + \ 369 | str(config.memdump_pid) + \ 370 | '.dmp' 371 | 372 | dump_file = crash_dir + \ 373 | os.sep + \ 374 | misc.addExtExe(os.path.basename(config.target)) + \ 375 | '.' + \ 376 | str(config.memdump_pid) + \ 377 | '_' + \ 378 | hash + \ 379 | '.dmp' 380 | 381 | dump_log = crash_dir + \ 382 | os.sep + \ 383 | misc.addExtExe(os.path.basename(config.target)) + \ 384 | '.' + \ 385 | str(config.memdump_pid) + \ 386 | '_' + \ 387 | hash + \ 388 | '.log' 389 | 390 | if(config.debug): 391 | print("dump file = %s\n" % dump_file) 392 | print("dump file (orig) = %s\n" % dump_file_orig) 393 | 394 | try: 395 | shutil.copy(dump_file_orig, dump_file) 396 | except Exception as error: 397 | if(config.debug): 398 | print("\n[ERROR] debug.win32Analyze() @ copy dump file: %s\n" % error) 399 | return FAILURE 400 | 401 | timeout = (config.maxtime * settings.DEBUG_TIMEOUT_MULTIPLE) 402 | 403 | cmdline = [] 404 | 405 | cmdline.append(settings.CONSOLE_DEBUGGER_PATH) 406 | 407 | cmdline.append('-kqm') 408 | cmdline.append('-nosqm') 409 | 410 | cmdline.append('-c') 411 | cmdline.append('.symfix; .reload; !analyze -v') 412 | 413 | cmdline.append('-z') 414 | cmdline.append(dump_file) 415 | 416 | # 417 | # pass dump file to console debugger and append output to fuzz info file 418 | # 419 | try: 420 | with open(dump_log, 'w') as file: 421 | process = subprocess.Popen(cmdline, 422 | stdin=None, 423 | stdout=file, 424 | stderr=file) 425 | 426 | (output, error) = process.communicate(timeout=timeout) 427 | except subprocess.TimeoutExpired as error: 428 | if(config.debug): 429 | print("%s\n" % error) 430 | 431 | misc.killProcess(process.pid) 432 | except Exception as error: 433 | if(config.debug): 434 | print("[ERROR] debug.win32Analyze() failed: %s\n" % error) 435 | return FAILURE 436 | 437 | return SUCCESS 438 | 439 | # 440 | # Crash handler for win32 441 | # 442 | def win32CrashHandler(event): 443 | event_code = event.get_event_code() 444 | 445 | # 446 | # catch interesting crashes 447 | # 448 | if(event_code == win32.EXCEPTION_DEBUG_EVENT and event.is_last_chance()): 449 | if(config.debug): 450 | print("entering win32CrashHandler() with event code %d (last chance)\n" % event_code) 451 | 452 | config.crash = True 453 | 454 | exception_name = event.get_exception_name() 455 | 456 | if(exception_name != 'EXCEPTION_ACCESS_VIOLATION'): 457 | fault_type = exception_name 458 | else: 459 | fault_type = event.get_fault_type() 460 | 461 | process = Process(event.get_pid()) 462 | cmdline = process.get_command_line() 463 | 464 | crash = Crash(event) 465 | info = crash.fullReport() 466 | 467 | if(config.debug): 468 | print("current_input: %s\n" % config.current_input) 469 | 470 | if(settings.MEMORY_DUMP): 471 | config.memdump_pid = event.get_pid() 472 | 473 | triage.win32(fault_type, cmdline, info) 474 | -------------------------------------------------------------------------------- /fuzz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # fuzz.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import glob 13 | import psutil 14 | import random 15 | import re 16 | import shutil 17 | import select 18 | import signal 19 | import ssl 20 | import time 21 | from tqdm import tqdm 22 | 23 | import core 24 | import debug 25 | import net 26 | import run 27 | import triage 28 | import mutator 29 | import misc 30 | import config 31 | import settings 32 | from settings import SUCCESS, FAILURE 33 | from settings import SIGABRT, SIGFPE, SIGSEGV 34 | 35 | # 36 | # where the magic begins 37 | # 38 | def main(cmdline, inputs): 39 | if(config.debug): 40 | print("entering fuzz.main()\n") 41 | 42 | if(misc.preChecks() == False): 43 | return FAILURE 44 | 45 | # 46 | # setup command line 47 | # 48 | if(cmdline != None): 49 | org_cmdline = cmdline 50 | cmdline = misc.setupCmdline(cmdline) 51 | else: 52 | org_cmdline = None 53 | 54 | # 55 | # network stuff 56 | # 57 | if(config.address != None): 58 | misc.setupAddress() 59 | 60 | if(None in (config.prot, config.host, config.port)): 61 | print("[ERROR] target address should be in proto://host:port format: %s\n" % config.address) 62 | return FAILURE 63 | 64 | if((misc.checkIPAddress(config.host) == False) and (misc.checkHostname(config.host) == False)): 65 | print("[ERROR] invalid ip or hostname: %s\n" % config.host) 66 | return FAILURE 67 | 68 | # 69 | # only do this check on remote servers as we're too early to have even started the local server 70 | # 71 | if(config.mode == settings.SERVER and config.prot == 'tcp'): 72 | if(config.prot == 'tcp'): 73 | if(misc.checkPort(config.prot, config.host, config.port) == False): 74 | print("[ERROR] connection failed to %s\n" % config.address) 75 | return FAILURE 76 | 77 | if(config.tls and (config.prot == 'udp')): 78 | print("[ERROR] TLS + UDP is not currently supported\n") 79 | return FAILURE 80 | 81 | # 82 | # optimize local client fuzzing speeds 83 | # 84 | if((config.insulate == False) and (config.mode == settings.LOCAL_CLIENT)): 85 | if(config.maxtime == 1): # only change "the default" for local clients 86 | if(misc.isWin32()): # win32 be more sensitive 87 | config.maxtime = (config.maxtime * 2) # slow it down 88 | else: 89 | config.maxtime = (config.maxtime / 4) # speed it up 90 | 91 | if(config.insulate or (config.mode == settings.LOCAL_SERVER)): 92 | config.try_again = True 93 | 94 | # 95 | # gather inputs 96 | # 97 | files = [] 98 | 99 | if(os.path.isdir(inputs)): 100 | inputs = os.path.abspath(inputs) 101 | 102 | if(misc.checkInputDir(inputs) != SUCCESS): 103 | return FAILURE 104 | 105 | try: 106 | files = glob.glob(inputs + '/*') 107 | except Exception as error: 108 | print("[ERROR] fuzz.main() @ glob: %s\n" % error) 109 | return FAILURE 110 | else: 111 | files.append(os.path.abspath(inputs)) 112 | 113 | # 114 | # if we made it this far, show stats 115 | # 116 | if(config.show_stats): 117 | misc.displayStats(org_cmdline, inputs, len(files), None) 118 | 119 | # 120 | # show progress 121 | # 122 | pb = tqdm(total=config.iterations, bar_format="{desc}") 123 | 124 | config.count = 0 125 | 126 | while config.count < config.iterations: 127 | if(config.down): 128 | if(config.debug): 129 | print("[INFO] config.down=True, breaking out of fuzzing run and exiting\n") 130 | break 131 | 132 | if(config.debug): 133 | print("-------------------------------- start iteration %d --------------------------------\n" % (config.count + 1)) 134 | 135 | if(config.down): 136 | return SUCCESS 137 | 138 | if(os.path.isfile(inputs) == False): 139 | chosen_input = random.choice(files) 140 | 141 | while(misc.checkAllowed(chosen_input) == False): 142 | if(config.debug): 143 | print("[INFO] skipping %s (special characters) as a chosen input\n" % os.path.basename(chosen_input)) 144 | 145 | chosen_input = random.choice(files) 146 | 147 | while(os.path.isfile(chosen_input) == False): 148 | if(config.debug): 149 | print("[INFO] skipping %s (not a file) as a chosen input\n" % os.path.basename(chosen_input)) 150 | 151 | chosen_input = random.choice(files) 152 | 153 | # 154 | # check the validity of chosen input before using it 155 | # 156 | if(misc.checkInput(chosen_input, files) != SUCCESS): 157 | continue # skip to next input 158 | # 159 | # otherwise, just use the given input file 160 | # 161 | else: 162 | chosen_input = files[0] 163 | 164 | if(misc.checkInput(chosen_input, files) != SUCCESS): 165 | return FAILURE 166 | 167 | config.current_input = chosen_input 168 | 169 | if(config.debug): 170 | print("\ninput: %s" % os.path.basename(chosen_input)) 171 | 172 | data = misc.readBytes(chosen_input) 173 | 174 | if(data == None): 175 | if(len(files) != 1): 176 | if(config.debug): 177 | print("[INFO] couldn't read %s -- skipping\n" % chosen_input) 178 | continue 179 | else: 180 | print("[ERROR] fuzz.main() @ reading the only input provided '%s': failed\n" % chosen_input) 181 | return FAILURE 182 | 183 | # 184 | # make sure FUZZ_FILE_PREV gets created so remote network triage doesn't break 185 | # 186 | if(cmdline == None): 187 | cmdline = ['FUZZ'] 188 | 189 | # 190 | # generate a unique fuzzing filename and set it in cmdline 191 | # 192 | if(misc.setupNewIteration(cmdline) == FAILURE): 193 | return FAILURE 194 | 195 | if(config.debug): 196 | print("\nwriting data to fuzz file @ %s\n" % settings.FUZZ_FILE) 197 | 198 | config.session = [] 199 | 200 | # 201 | # call mutators with consideration of input options 202 | # 203 | if(config.multibin): 204 | if(config.debug): 205 | print("multibin session fuzzing enabled\n") 206 | 207 | if(misc.setupSession(files, None) != SUCCESS): 208 | return FAILURE 209 | 210 | if(config.multinum == None): 211 | n = misc.getRandomInt(0, (len(config.session) - 1)) 212 | else: 213 | if((config.multinum > 0) and (config.multinum <= len(config.session))): 214 | n = (config.multinum - 1) # user counts from 1, machine counts from 0 215 | else: 216 | if(config.debug): 217 | print("[INFO] invalid multinum=%d, reverting back to choosing a random input\n" % config.multinum) 218 | 219 | n = misc.getRandomInt(0, (len(config.session) - 1)) 220 | 221 | # n = 1 # CHANGE: fuzz packet 2 only 222 | 223 | if(config.nofuzz == False): 224 | if(config.debug): 225 | print("%s\n" % config.session[n]) 226 | 227 | try: 228 | config.session[n] = misc.getMutant(config.session[n]) 229 | except Exception as error: 230 | print("[ERROR] fuzz.main() @ getMutant for binary session: %s\n" % error) 231 | return FAILURE 232 | 233 | misc.writeBytes(settings.FUZZ_FILE, config.session) 234 | elif(config.multistr): 235 | if(config.debug): 236 | print("multistr session fuzzing enabled\n") 237 | 238 | if(misc.setupSession(None, data) != SUCCESS): 239 | return FAILURE 240 | 241 | # 242 | # testing for multistr /w --multinum, eg. FTP would be good 243 | # 244 | 245 | if(config.multinum == None): 246 | n = misc.getRandomInt(0, (len(config.session) - 1)) 247 | else: 248 | if((config.multinum > 0) and (config.multinum <= len(config.session))): 249 | n = (config.multinum - 1) 250 | else: 251 | if(config.debug): 252 | print("[INFO] invalid multinum=%d, reverting back to choosing a random input\n" % config.multinum) 253 | 254 | n = misc.getRandomInt(0, (len(config.session) - 1)) 255 | 256 | s = bytearray(config.session[n].encode()) 257 | 258 | if(config.nofuzz == False): 259 | try: 260 | config.session[n] = misc.getMutant(s).decode(errors='replace') 261 | except Exception as error: 262 | print("[ERROR] fuzz.main() @ getMutant for string session: %s\n" % error) 263 | return FAILURE 264 | 265 | mutant = ''.join(config.session).encode() 266 | 267 | misc.writeBytes(settings.FUZZ_FILE, mutant) # fuzzing uses config.session, file is for artifacts only 268 | else: 269 | mutant = None 270 | data = ''.join(config.session).encode() 271 | 272 | misc.writeBytes(settings.FUZZ_FILE, data) 273 | else: 274 | if(config.nofuzz == False): 275 | mutant = misc.getMutant(data) 276 | 277 | if(mutant == None): 278 | print("[ERROR] fuzz.main() @ mutant=None: mutation failed\n") 279 | return FAILURE 280 | 281 | misc.writeBytes(settings.FUZZ_FILE, mutant) 282 | else: 283 | mutant = None 284 | misc.writeBytes(settings.FUZZ_FILE, data) 285 | 286 | config.count += 1 287 | 288 | # 289 | # adventure time 290 | # 291 | if(config.mode == settings.LOCAL): 292 | run.main(cmdline) 293 | elif((config.mode == settings.LOCAL_CLIENT) or (config.mode == settings.LOCAL_SERVER)): 294 | net.main(cmdline) 295 | 296 | if(misc.isUnix()): 297 | if(misc.copyDebugOutput() != SUCCESS): 298 | if(config.debug): 299 | print("[ERROR] misc.postIteration() @ copyDebugOutput() failed\n") 300 | return FAILURE 301 | 302 | if(config.mode == settings.LOCAL_SERVER): # don't call checkDebugger() on local client 303 | triage.checkDebugger(cmdline) 304 | else: # remote client and server mode 305 | if(config.attach or config.report_crash): # hybrid features 306 | net.main(cmdline) 307 | else: # no visibility, handle crashes without triage 308 | # if(net.main(cmdline) != SUCCESS and config.count > 1): 309 | result = net.main(cmdline) 310 | 311 | if((result != SUCCESS) and (config.count > 1)): 312 | misc.clientServerCrash() 313 | misc.displayCount(pb, 0, True) 314 | 315 | if(config.prot == 'tcp'): # tcp server support only 316 | if(settings.TCP_KEEP_GOING and (config.count != config.iterations)): 317 | print("\n\n[!] check if target down, sleeping %d seconds before trying to continue fuzzing...\n" % (settings.NET_SLEEP_TIME * 3)) 318 | time.sleep(settings.NET_SLEEP_TIME * 3) 319 | 320 | if(misc.checkPort(config.prot, config.host, config.port) == False): 321 | break 322 | 323 | config.down = False 324 | else: 325 | break 326 | else: 327 | break 328 | else: 329 | if(misc.checkPort(config.prot, config.host, config.port) == False): # check if server is down 330 | 331 | misc.clientServerCrash() 332 | misc.displayCount(pb, 0, True) 333 | 334 | if(config.prot == 'tcp'): # tcp server support only 335 | if(settings.TCP_KEEP_GOING and (config.count != config.iterations)): 336 | print("\n\n[!] check if target down, sleeping %d seconds before trying to continue fuzzing...\n" % (settings.NET_SLEEP_TIME * 3)) 337 | time.sleep(settings.NET_SLEEP_TIME * 3) 338 | 339 | # if(misc.checkPort(config.prot, config.host, config.port) == False): 340 | # break 341 | 342 | # config.down = False 343 | else: 344 | break 345 | else: 346 | break 347 | 348 | misc.displayCount(pb, 0, True) 349 | 350 | if(misc.postIteration(cmdline) != SUCCESS): 351 | return FAILURE # something happened, exit fuzzing 352 | 353 | if(config.debug): 354 | print("-------------------------------- end iteration %d --------------------------------\n" % (config.count)) 355 | 356 | pb.close() 357 | 358 | if((config.mode == settings.SERVER) and (config.prot == 'tcp')): 359 | if(misc.checkPort(config.prot, config.host, config.port) == False): 360 | if((config.attach == None) and (config.report_crash == False)): 361 | config.down = True 362 | 363 | misc.killTargetProcesses() 364 | 365 | return SUCCESS 366 | -------------------------------------------------------------------------------- /input/doc/test.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sec-tools/litefuzz/299f5f9b8da1b349cb181f2a61bc5053403002d8/input/doc/test.doc -------------------------------------------------------------------------------- /input/doc/test.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sec-tools/litefuzz/299f5f9b8da1b349cb181f2a61bc5053403002d8/input/doc/test.docx -------------------------------------------------------------------------------- /input/ftp/req.txt: -------------------------------------------------------------------------------- 1 | USER test 2 | PASS Password1 3 | PWD 4 | SYST 5 | PORT 10,10,10,23,233,131 6 | LIST 7 | QUIT 8 | -------------------------------------------------------------------------------- /input/ftp/resp.txt: -------------------------------------------------------------------------------- 1 | 220 ProFTPD Server (Debian) [::ffff:localhost] 2 | 331 Password required for user 3 | 230 User user logged in 4 | 257 "/" is your current location 5 | 200 TYPE is now 8-bit binary 6 | 227 Entering Passive Mode (195,154,194,249,172,166) 7 | 200 PORT command successful. 8 | 150 Opening ASCII mode data 9 | 226 Transfer OK 10 | 221 Goodbye 11 | -------------------------------------------------------------------------------- /input/http/req.txt: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host: localhost 3 | 4 | -------------------------------------------------------------------------------- /input/http/resp.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: Apache 3 | Accept-Ranges: bytes 4 | Content-Length: 12 5 | Content-Type: text/html 6 | Connection: close 7 | 8 | Hello world! 9 | 10 | -------------------------------------------------------------------------------- /input/ssh-resp/1: -------------------------------------------------------------------------------- 1 | SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2 2 | -------------------------------------------------------------------------------- /input/ssh-resp/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sec-tools/litefuzz/299f5f9b8da1b349cb181f2a61bc5053403002d8/input/ssh-resp/2 -------------------------------------------------------------------------------- /input/ssh-resp/3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sec-tools/litefuzz/299f5f9b8da1b349cb181f2a61bc5053403002d8/input/ssh-resp/3 -------------------------------------------------------------------------------- /input/tex/test.tex: -------------------------------------------------------------------------------- 1 | % simple file for testing models 2 | 3 | \documentclass[12pt]{article} 4 | 5 | \title{} 6 | \author{} 7 | \date{} 8 | 9 | \input{cspogil.sty} 10 | 11 | \begin{document} 12 | 13 | \setcounter{section}{2} 14 | \setcounter{question}{5} 15 | 16 | \def\Teacher{} 17 | 18 | \makeatletter 19 | \def\input@path{{CS1/Act01/}} 20 | \graphicspath{{CS1/Act01/}} 21 | \makeatother 22 | 23 | \input{hello-world.tex} 24 | 25 | \end{document} 26 | -------------------------------------------------------------------------------- /litefuzz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -Wignore 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz.py 5 | # 6 | # litefuzz project 7 | # 8 | # A multi-platform fuzzer for poking at userland binaries and servers 9 | # 10 | # 11 | 12 | import os 13 | import platform 14 | import sys 15 | import signal 16 | 17 | import core 18 | import fuzz 19 | import misc 20 | import cli 21 | import config 22 | import settings 23 | from settings import SUCCESS, FAILURE 24 | 25 | class LiteFuzzer(object): 26 | def __init__(self, args): 27 | self.local = args.local 28 | self.client = args.client 29 | self.server = args.server 30 | self.cmdline = args.cmdline 31 | self.inputs = args.inputs 32 | self.iterations = args.iterations 33 | self.mutator = args.mutator 34 | self.address = args.address 35 | self.crashdir = args.crashdir 36 | self.tempdir = args.tempdir 37 | self.fuzzfile = args.fuzzfile 38 | self.maxtime = args.maxtime 39 | self.minfile = args.minfile 40 | self.supermin = args.supermin 41 | self.reprofile = args.reprofile 42 | self.reuse = args.reuse 43 | self.multibin = args.multibin 44 | self.multistr = args.multistr 45 | self.multinum = args.multinum 46 | self.insulate = args.insulate 47 | self.nofuzz = args.nofuzz 48 | self.key = args.key 49 | self.golang = args.golang 50 | self.tls = args.tls 51 | self.attach = args.attach 52 | self.cmd = args.cmd 53 | self.rmfile = args.rmfile 54 | self.reportcrash = args.reportcrash 55 | self.memdump = args.memdump 56 | self.nomemdump = args.nomemdump 57 | self.malloc = args.malloc 58 | self.nomalloc = args.nomalloc 59 | self.debug = args.debug 60 | 61 | def run(self): 62 | if(self.debug): 63 | config.debug = True 64 | 65 | if(config.debug): 66 | print("I'm running on %s with python %s\n" % (sys.platform, platform.python_version())) 67 | 68 | if(self.supermin != None): 69 | self.minfile = self.supermin 70 | config.supermin = True 71 | 72 | # 73 | # set mode 74 | # 75 | if(self.local and self.client): 76 | config.mode = settings.LOCAL_CLIENT 77 | elif(self.client): 78 | config.mode = settings.CLIENT 79 | elif(self.local and self.server): 80 | config.mode = settings.LOCAL_SERVER 81 | elif(self.server): 82 | config.mode = settings.SERVER 83 | elif(self.local): 84 | config.mode = settings.LOCAL 85 | else: 86 | if((self.minfile == None) and (self.reprofile == None)): 87 | print("[ERROR] must choose a fuzzing mode\n") 88 | return FAILURE 89 | 90 | if(self.reuse and (config.mode != settings.LOCAL)): 91 | print("[ERROR] reuse mode is only supported for local targets\n") 92 | return FAILURE 93 | 94 | if(config.debug): 95 | print("mode is %d\n" % config.mode) 96 | 97 | if((config.mode != settings.CLIENT) and (config.mode != settings.SERVER)): 98 | if(self.cmdline == None): 99 | print("[ERROR] local runs need cmdline\n") 100 | return FAILURE 101 | 102 | if(config.mode != settings.LOCAL): 103 | if(self.address == None): 104 | print("[ERROR] client or server modes need a target address containing host:port\n") 105 | return FAILURE 106 | else: 107 | config.address = self.address 108 | 109 | # 110 | # check for min or repro 111 | # 112 | if((self.minfile == None) and (self.reprofile == None)): 113 | config.fuzz = True 114 | else: 115 | if(self.minfile != None): 116 | config.min = True 117 | 118 | if(self.reprofile != None): 119 | config.repro = True 120 | 121 | # 122 | # allow users to just turn memory debugging on/off without fuzzing 123 | # 124 | if(self.inputs == None): 125 | if((config.min == False) and (config.repro == False)): 126 | if(misc.isWin32()): 127 | if((self.malloc == False) and (self.nomalloc == False)): 128 | if((self.memdump == False) and (self.nomemdump == False)): 129 | print("[ERROR] input directory is required\n") 130 | return FAILURE 131 | else: 132 | print("[ERROR] input directory is required\n") 133 | return FAILURE 134 | else: 135 | config.inputs = self.inputs # useful for determining fuzzing or config modes (eg. doHeapCheck) 136 | 137 | if(self.inputs != None): 138 | if((os.path.isdir(self.inputs)) == False and (os.path.isfile(self.inputs) == False)): 139 | print("[ERROR] %s is not a valid directory or file\n" % self.inputs) 140 | return FAILURE 141 | 142 | config.inputs = self.inputs 143 | 144 | if(config.min == False): 145 | config.iterations = self.iterations 146 | 147 | settings.MUTATOR_CHOICE = self.mutator 148 | 149 | if(self.crashdir != None): 150 | settings.CRASH_DIR = self.crashdir 151 | 152 | if(os.path.isdir(settings.CRASH_DIR) == False): 153 | try: 154 | os.makedirs(settings.CRASH_DIR) 155 | except Exception as error: 156 | print("[ERROR] could not create '%s' directory: %s\n" % (settings.CRASH_DIR, error)) 157 | return FAILURE 158 | 159 | if(self.tempdir != None): 160 | settings.TMP_DIR = self.tempdir 161 | 162 | # 163 | # make the tmp directory if it doesn't exist 164 | # 165 | if(os.path.isdir(settings.TMP_DIR) == False): 166 | try: 167 | os.makedirs(settings.TMP_DIR) 168 | except Exception as error: 169 | print("[ERROR] could not create '%s' directory: %s\n" % (settings.TMP_DIR, error)) 170 | return FAILURE 171 | 172 | if(misc.setupTmpRunDir() == False): 173 | return FAILURE 174 | 175 | if(self.fuzzfile != None): 176 | try: 177 | open(self.fuzzfile, 'wb') 178 | except Exception as error: 179 | print("[ERROR] invalid fuzz file '%s': %s\n" % (self.fuzzfile, error)) 180 | return FAILURE 181 | 182 | config.static_fuzz_file = self.fuzzfile 183 | 184 | config.maxtime = float(self.maxtime) 185 | config.multibin = self.multibin 186 | config.multistr = self.multistr 187 | config.multinum = self.multinum 188 | config.insulate = self.insulate 189 | config.nofuzz = self.nofuzz 190 | config.key = self.key 191 | config.golang = self.golang 192 | config.tls = self.tls 193 | config.attach = self.attach 194 | config.cmd = self.cmd 195 | config.rmfile = self.rmfile 196 | 197 | if(config.insulate and misc.isWin32()): 198 | print("[ERROR] the insulate feature on Windows is not supported\n") 199 | return FAILURE 200 | 201 | # 202 | # note: you can still fuzz localhost as a "remote" target with attach (the lines blur a bit here) 203 | # 204 | if(config.attach): 205 | if(config.mode == settings.LOCAL): 206 | print("[ERROR] --attach currently only supports network fuzzing (choose a non-local for this one)\n") 207 | return FAILURE 208 | 209 | config.target = config.attach 210 | 211 | if(self.reportcrash != None): 212 | config.report_crash = True 213 | 214 | if(self.cmdline == None): # otherwise target is derived from cmdline 215 | config.target = self.reportcrash 216 | 217 | if(misc.isMac() == False): 218 | print("[ERROR] reportcrash is available on Mac only\n") 219 | return FAILURE 220 | 221 | if(misc.checkReportCrashRunning() == False): 222 | print("[ERROR] failed to turn on ReportCrash, please enable it and try again\n") 223 | return FAILURE 224 | 225 | misc.checkReportCrashDirectory() 226 | misc.checkReportCrashFiles() 227 | 228 | if(misc.isWin32()): 229 | if((config.mode != settings.CLIENT) and (config.mode != settings.SERVER)): 230 | target = misc.setupCmdline(self.cmdline)[0] 231 | 232 | if(misc.checkMemoryDump(target)): # if memdumps are already enabled, let the fuzzer know 233 | settings.MEMORY_DUMP = True 234 | 235 | if(self.memdump or self.nomemdump): 236 | if(self.memdump): 237 | if(misc.doMemoryDump(target, True) == SUCCESS): 238 | if(self.inputs == None): 239 | print("\nMemory dumps turned ON for %s" % target) 240 | return SUCCESS 241 | else: 242 | print("Failed to turn on memory dumps for %s" % target) 243 | 244 | if(self.inputs == None): 245 | return FAILURE 246 | else: # nomemdump 247 | if(misc.doMemoryDump(target, False) == SUCCESS): 248 | if(self.inputs == None): 249 | print("\nMemory dumps turned OFF for %s" % target) 250 | return SUCCESS 251 | else: 252 | print("Failed to turn off memory dumps for %s" % target) 253 | 254 | if(self.inputs == None): 255 | return FAILURE 256 | 257 | if((config.mode != settings.CLIENT) and (config.mode != settings.SERVER)): 258 | if(self.malloc): 259 | if(misc.isLinux()): 260 | if(self.malloc == 'default'): 261 | settings.DEBUG_ENV_EFENCE_GLIBC_ENABLE = True 262 | elif(self.malloc == 'glibc'): 263 | settings.DEBUG_ENV_GLIBC_ENABLE = True 264 | else: 265 | settings.DEBUG_ENV_EFENCE_ENABLE = True 266 | elif(misc.isMac()): 267 | settings.DEBUG_ENV_GMALLOC_ENABLE = True 268 | elif(misc.isWin32()): 269 | settings.DEBUG_ENV_PAGEHEAP_ENABLE = True 270 | 271 | if(self.inputs == None): 272 | target = misc.setupCmdline(self.cmdline)[0] 273 | 274 | if(misc.doPageHeap(target, True) == SUCCESS): 275 | print("\nPageHeap turned ON for %s" % target) 276 | return SUCCESS 277 | else: 278 | print("Failed to turn on PageHeap on for %s" % target) 279 | return FAILURE 280 | else: 281 | print("\n[INFO] -z ignored, unsupported platform\n") 282 | 283 | if(self.nomalloc): 284 | settings.DEBUG_ENV_PAGEHEAP_DISABLE = True 285 | 286 | if(misc.isWin32()): 287 | if(self.inputs == None): 288 | target = misc.setupCmdline(self.cmdline)[0] 289 | 290 | if(misc.doPageHeap(target, False) == SUCCESS): 291 | print("\nPageHeap turned OFF for %s" % target) 292 | return SUCCESS 293 | else: 294 | print("Failed to turn off PageHeap for %s" % target) 295 | return FAILURE 296 | else: 297 | print("\n[INFO] -zz ignored, memory debugging is set at runtime for non-Windows OS\n") 298 | 299 | if(config.mode == settings.LOCAL): # file or stdin fuzzing 300 | if(misc.isWin32() and ('FUZZ' not in self.cmdline)): # stdin fuzzing unsupported on win32 301 | print("[ERROR] keyword FUZZ required in cmdline (stdin fuzzing not supported on Windows)\n") 302 | return FAILURE 303 | 304 | if(config.debug): 305 | print("[INFO] tmp dir: %s\n" % settings.TMP_DIR) 306 | 307 | if(settings.KEEPAWAKE_ENABLE): 308 | misc.keepAwake() 309 | 310 | if(misc.isMac()): 311 | misc.keepAwake() 312 | 313 | misc.setupEnv() 314 | 315 | print("--========================--") 316 | print("--======| litefuzz |======--") 317 | print("--========================--\n") 318 | 319 | if(config.fuzz): 320 | ret = fuzz.main(self.cmdline, self.inputs) 321 | if(self.reuse): 322 | if(ret == FAILURE): 323 | return FAILURE 324 | 325 | if(misc.checkReuse(self.inputs)): 326 | print("\n[+] reusing crashes to grind on...\n") 327 | 328 | config.show_stats = False 329 | 330 | ret = fuzz.main(self.cmdline, os.path.relpath(config.reusedir)) 331 | 332 | config.iterations += self.iterations 333 | 334 | elif(config.min): 335 | ret = core.minimize(self.minfile, 336 | self.cmdline, 337 | self.malloc, 338 | self.address) 339 | 340 | if(ret != SUCCESS): 341 | return FAILURE 342 | 343 | if(config.supermin and (config.min_hit == False)): 344 | print("[+] supermin activated, continuing...\n") 345 | 346 | config.show_stats = False 347 | 348 | while(config.min_hit == False): 349 | ret = core.minimize(config.current_input, # kinda like reuse 350 | self.cmdline, 351 | self.malloc, 352 | self.address) 353 | 354 | elif(config.repro): 355 | ret = core.repro(self.reprofile, 356 | self.cmdline, 357 | self.malloc, 358 | self.address) 359 | else: 360 | return FAILURE 361 | 362 | if(ret != SUCCESS): 363 | return FAILURE 364 | 365 | if(config.mode != settings.CLIENT): 366 | misc.displayResults() 367 | 368 | if(config.debug == False): 369 | misc.cleanupTmp() 370 | 371 | return SUCCESS 372 | 373 | def main(): 374 | global org_sigint 375 | 376 | signal.signal(signal.SIGINT, misc.pauseFuzzer) 377 | 378 | args = cli.arg_parse() 379 | lf = LiteFuzzer(args) 380 | 381 | result = lf.run() 382 | 383 | if(result > 0): 384 | sys.exit(FAILURE) 385 | 386 | if(__name__ == '__main__'): 387 | main() 388 | -------------------------------------------------------------------------------- /mutator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # mutator.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import random 13 | 14 | import misc 15 | import config 16 | import settings 17 | 18 | if(str(sys.platform).startswith('linux')): 19 | try: 20 | import pyradamsa 21 | except: 22 | settings.RADAMSA_MUTATOR_ENABLE = False 23 | 24 | # 25 | # simple mutator that flips random bytes at a random indicies 26 | # 27 | def flip(data, mutations): 28 | if(config.debug): 29 | print("\n[MUTATOR] flip\n") 30 | print("mutations: %d\n" % mutations) 31 | 32 | # 33 | # flip a different index for each mutation 34 | # 35 | for x in range(mutations): 36 | i = misc.getRandomInt(0, (len(data) - 1)) 37 | data[i] = misc.getRandomInt(0, 255) 38 | 39 | return data 40 | 41 | # 42 | # flip 1-4 random bytes at a random index with common "magic" high and low values 43 | # 44 | def highLow(data): 45 | if(config.debug): 46 | print("\n[MUTATOR] highLow\n") 47 | 48 | # 49 | # 00, 01, 02, 03, 7E, 7F, 80, 81, FC, FD, FE, FF 50 | # 51 | magic = (0, 1, 2, 3, 126, 127, 128, 129, 252, 253, 254, 255) 52 | 53 | size = misc.getRandomInt(1, 4) # random small size 54 | 55 | if(len(data) == 1): 56 | i = 0 57 | else: 58 | i = misc.getRandomInt(0, (len(data) - 1)) # random index 59 | 60 | x = misc.getWithin(i, len(data), size) # random size 61 | b = random.choice(magic) # random magic 62 | 63 | c = 0 64 | 65 | if(config.debug): 66 | print("x=%d, i=%d, b=%d\n" % (x, i, b)) 67 | 68 | while(c < x): 69 | data[i + c] = b 70 | c += 1 71 | 72 | return data 73 | 74 | # 75 | # Insert a random number of random bytes at a random location 76 | # 77 | def insert(data): 78 | if(len(data) == 1): 79 | i = 0 80 | else: 81 | i = misc.getRandomInt(0, (len(data) - 1)) # random index 82 | 83 | size = misc.getRandomInt(settings.SIZE_MIN, settings.SIZE_MAX) # random insert size 84 | 85 | if(config.debug): 86 | print("\n[MUTATOR] insert\n") 87 | print("len(data): %d" % len(data)) 88 | print("size: %d\n" % size) 89 | 90 | # 91 | # original data len + size 92 | # 93 | mutant = bytearray(len(data) + size) 94 | 95 | if(config.debug): 96 | print("mutant=%d\n" % len(mutant)) 97 | print("size=%d @ i=%d\n" % (size, i)) 98 | 99 | c = 0 100 | 101 | # 102 | # copy data up until index 103 | # 104 | try: 105 | while(c < i): 106 | mutant[c] = data[c] 107 | c += 1 108 | except Exception as error: 109 | print("\n[ERROR] misc.insertMutator() @ initial copy: %s\n" % error) 110 | return None 111 | 112 | c = 0 113 | 114 | # 115 | # two methods here: pick a random byte or make them all random bytes 116 | # 117 | b = misc.getRandomInt(0, 255) 118 | method = misc.getRandomInt(1, 2) 119 | 120 | if(config.debug): 121 | print("b=%d, method=%d\n" % (b, method)) 122 | 123 | # 124 | # insert mutation 125 | # 126 | if(method == 1): 127 | try: 128 | while(c < size): 129 | mutant[i + c] = b 130 | c += 1 131 | except Exception as error: 132 | print("\n[ERROR] misc.insertMutator() @ method=%d: %s\n" % (method, error)) 133 | return None 134 | if(method == 2): 135 | try: 136 | while(c < size): 137 | mutant[i + c] = misc.getRandomInt(0, 255) 138 | c += 1 139 | except Exception as error: 140 | print("\n[ERROR] misc.insertMutator() @ method=%d: %s\n" % (method, error)) 141 | return None 142 | 143 | # 144 | # copy starting at mutant index + size the rest of data index 145 | # 146 | 147 | if(config.debug): 148 | print("mutant[%d] = data[%d]\n" % ((i + size + c), (i + c))) 149 | 150 | try: 151 | while (i < len(data)): 152 | mutant[i + size] = data[i] 153 | i += 1 154 | except Exception as error: 155 | print("\n[ERROR] misc.insertMutator() @ final copy: %s\n" % error) 156 | return None 157 | 158 | return mutant # not data 159 | 160 | # 161 | # remove random number of bytes at a random index 162 | # 163 | def remove(data): 164 | if(config.debug): 165 | print("\n[MUTATOR] remove\n") 166 | print("len(data): %d" % len(data)) 167 | 168 | size = misc.getRandomSize(settings.SIZE_MIN, data) 169 | 170 | if(len(data) == 1): 171 | i = 0 172 | else: 173 | i = misc.getRandomInt(0, (len(data) - 1)) # random index 174 | 175 | x = misc.getWithin(i, len(data), size) # random size 176 | 177 | mutant = data 178 | 179 | if(config.debug): 180 | print("i=%d @ x=%d\n" % (i, x)) 181 | 182 | c = 0 183 | 184 | # 185 | # keep removing bytes at index until X is hit 186 | # 187 | while(c < x): 188 | mutant.pop(i + c) 189 | 190 | c += 1 191 | x -= 1 192 | 193 | return mutant 194 | 195 | # 196 | # Carve (or slice) out a chunk of bytes at a random location 197 | # 198 | def carve(data): 199 | if(config.debug): 200 | print("\n[MUTATOR] carve\n") 201 | print("len(data): %d" % len(data)) 202 | 203 | i = 0 204 | o = 0 205 | 206 | if(len(data) == 1): 207 | i = 0 208 | o = 0 209 | else: 210 | while((i == 0) and (o == 0)): 211 | i = misc.getRandomInt(0, (len(data) - 1)) # random index 212 | o = misc.getRandomInt(0, (len(data) - 1)) # another random index 213 | 214 | # 215 | # original data len + x 216 | # 217 | mutant = bytearray(len(data)) 218 | 219 | if(config.debug): 220 | print("mutant=%d\n" % len(mutant)) 221 | print("i=%d, o=%d\n" % (i, o)) 222 | 223 | # 224 | # get random slice (carving?) 225 | # 226 | if(i < o): 227 | mutant = data[i:o] 228 | elif(o < i): 229 | mutant = data[o:i] 230 | else: 231 | mutant = data[:o] # we like to have fun here 232 | 233 | return mutant # not data 234 | 235 | # 236 | # Overwrite a random number of random bytes at a random location (without overrun of input size) 237 | # 238 | # note: this mutator is slower than the others (more noticable on fast targets with big inputs) 239 | # 240 | # data = mutant data 241 | # size = random(len(data)) -> random number of bytes, finalizes into x (with consideration of index) 242 | # 243 | def overwrite(data): 244 | size = misc.getRandomSize(settings.SIZE_MIN, data) 245 | 246 | if(config.debug): 247 | print("\n[MUTATOR] overwrite\n") 248 | print("len(data): %d" % len(data)) 249 | print("size: %d\n" % size) 250 | 251 | if(len(data) == 1): 252 | i = 0 253 | x = 1 254 | else: 255 | i = misc.getRandomInt(0, size) # random index 256 | 257 | # 258 | # ensure we can a size that will fit (considering the index) 259 | # 260 | x = misc.getWithin(i, len(data), size) # random size 261 | 262 | c = 0 263 | 264 | # 265 | # two methods here: pick a random byte or make them all random bytes 266 | # 267 | b = misc.getRandomInt(0, 255) 268 | 269 | method = misc.getRandomInt(1, 2) 270 | 271 | if(config.debug): 272 | print("x=%d @ i=%d, b=%s, method=%d\n" % (x, i, b, method)) 273 | 274 | if(method == 1): 275 | try: 276 | while(c < x): 277 | data[i + c] = b 278 | c += 1 279 | except Exception as error: 280 | print("\n[ERROR]: %s\n" % error) 281 | else: 282 | try: 283 | while(c < x): 284 | data[i + c] = misc.getRandomInt(0, 255) 285 | c += 1 286 | except Exception as error: 287 | print("\n[ERROR]: %s\n" % error) 288 | 289 | return data 290 | 291 | # 292 | # Use radamsa's mutation engine 293 | # 294 | def radamsa(data): 295 | if(config.debug): 296 | print("\n[MUTATOR] radamsa\n") 297 | 298 | radamsa = pyradamsa.Radamsa() 299 | data = bytearray(radamsa.fuzz(data)) 300 | 301 | return data 302 | -------------------------------------------------------------------------------- /net.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # net.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import re 13 | import shutil 14 | from datetime import datetime 15 | import signal 16 | import socket 17 | import ssl 18 | import subprocess32 as subprocess 19 | import time 20 | from time import time as timer 21 | from tqdm import tqdm 22 | import threading 23 | 24 | try: 25 | ConnectionRefusedError # py3 26 | except NameError: 27 | ConnectionRefusedError = socket.error # py2 28 | 29 | import core 30 | import run 31 | import debug 32 | import triage 33 | import misc 34 | import config 35 | import settings 36 | from settings import SUCCESS, FAILURE 37 | 38 | # 39 | # local client / server fuzzing and crash replay for debugging 40 | # 41 | def main(cmdline): 42 | if(config.debug): 43 | print("entering net.main()\n") 44 | 45 | startTime = timer() 46 | 47 | # 48 | # setup TLS 49 | # 50 | if(config.tls): 51 | try: 52 | if((config.mode == settings.SERVER) or (config.mode == settings.LOCAL_SERVER)): 53 | config.context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 54 | else: # client 55 | config.context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) 56 | 57 | config.context.check_hostname = False 58 | config.context.verify_mode = ssl.CERT_NONE 59 | config.context.load_cert_chain(settings.NETWORK_CRT, keyfile=settings.NETWORK_KEY) 60 | except Exception as error: 61 | print("\n[ERROR] net.main() @ SSL create context: %s\n" % error) 62 | return FAILURE 63 | 64 | if((config.mode == settings.CLIENT) or (config.mode == settings.LOCAL_CLIENT)): 65 | startServer(cmdline, False) # start built-in server to fuzz target client 66 | else: 67 | startClient(cmdline, False) # start built-in client to fuzz target server 68 | 69 | if(config.conn == None): 70 | print("\n[ERROR] net.main() @ socket: likely a timeout or SSL issue, try running it again\n") 71 | misc.killTargetProcesses() 72 | sys.exit(FAILURE) 73 | 74 | # 75 | # *now for the fun part* 76 | # 77 | # four scenarios which may be signs of a crash 78 | # 79 | # 1) we can't connect to the target anymore (previous mutant) 80 | # 2) we can connect, but cannot send data (previous mutant) 81 | # 3) we can send data, but recv zero bytes (current mutant) 82 | # 4) we can connect, but cannot send data AFTER sending data (current mutant) 83 | # 84 | 85 | # 86 | # crash scenario #1 87 | # 88 | # connect to target 89 | # 90 | # -> if connect fails, check for crash by repro'ing PREVIOUS fuzz file 91 | # 92 | if(config.mode == settings.SERVER or (config.mode == settings.LOCAL_SERVER)): 93 | try: 94 | config.conn.connect((config.host, config.port)) 95 | except Exception as error: 96 | if(config.count == 1): 97 | if(config.mode == settings.LOCAL_SERVER): 98 | if(config.debug): 99 | print("[INFO] connection failed, giving the target a few more seconds to spin up\n") 100 | 101 | time.sleep(settings.LOCAL_SERVER_TIMEOUT) 102 | 103 | try: 104 | if(config.tls == False): # we're already connected if using TLS 105 | config.conn.connect((config.host, config.port)) 106 | except Exception as error: 107 | print("\n[ERROR] net.main() @ connect: %s\n" % error) 108 | misc.killTargetProcesses() 109 | sys.exit(FAILURE) 110 | else: 111 | if(config.debug): 112 | print("\n[INFO] failed to make a connection to target\n") 113 | 114 | if((config.mode == settings.CLIENT) or (config.mode == settings.SERVER)): 115 | if((config.attach == None) and (config.report_crash == False)): 116 | config.down = True # this var is too useful :') 117 | misc.clientServerCrash() 118 | else: 119 | if(misc.isUnix()): 120 | if(core.reproCrash(settings.FUZZ_FILE_PREV, cmdline, False) != SUCCESS): 121 | if(config.debug): 122 | print("[INFO] couldn't repro crash @ initial connect\n") 123 | return FAILURE 124 | else: 125 | return SUCCESS 126 | 127 | # 128 | # support connect() -> attaching to the process in a debugger 129 | # 130 | # note: this supports the general scenarion and also processes that launchd spawns 131 | # 132 | if(config.attach): 133 | time.sleep(1) # give it a second in case it's a parent process forking a child 134 | 135 | if(config.attach.isnumeric()): 136 | pid = config.attach 137 | 138 | if(config.debug): 139 | print("calling debug.attach() for pid=%s\n" % pid) 140 | else: 141 | pid = misc.processPid(config.attach) 142 | 143 | if(pid == None): 144 | print("[ERROR] net.main() @ attach failed for %s: couldn't get pid\n" % config.attach) 145 | misc.killTargetProcesses() 146 | sys.exit(FAILURE) 147 | 148 | if(config.debug): 149 | print("calling debug.attach() for name=%s pid=%s\n" % (config.attach, pid)) 150 | 151 | debug.attach('lldb', pid) 152 | 153 | # 154 | # initial recv() 155 | # 156 | buf = None 157 | 158 | try: 159 | if(config.prot == 'tcp'): 160 | buf = config.conn.recv(settings.RECV_SIZE) 161 | else: 162 | (buf, address) = config.conn.recvfrom(settings.RECV_SIZE) 163 | except Exception as error: 164 | if(config.debug): 165 | print("\n[INFO] net.main() @ initial recv(): %s\n" % error) 166 | 167 | if(config.debug): 168 | if(buf): 169 | print("recv from target:\n%s\n" % buf) 170 | 171 | if(config.multibin or config.multistr): 172 | if(len(config.session) == 0): 173 | print("\n[ERROR] no session data to fuzz, check inputs\n") 174 | return FAILURE 175 | 176 | for s in config.session: 177 | if(config.multibin): 178 | sb = s # multibin is already bytes 179 | else: 180 | sb = bytearray(s.encode()) 181 | 182 | # 183 | # standard send/recv bytes 184 | # 185 | if(misc.sendRecvBytes(sb) != SUCCESS): 186 | if(config.mode == settings.LOCAL_SERVER): 187 | if(misc.isUnix()): # no repro with local server on win32 188 | if(core.reproCrash(settings.FUZZ_FILE_PREV, cmdline, False) != SUCCESS): 189 | if(config.debug): 190 | print("[INFO] couldn't repro crash @ sendRecvBytes(sb)\n") 191 | return FAILURE 192 | else: 193 | return SUCCESS 194 | elif((config.mode == settings.CLIENT) or (config.mode == settings.SERVER)): 195 | if(config.broken_pipe): 196 | config.broken_pipe = False 197 | return SUCCESS 198 | else: 199 | return FAILURE 200 | else: 201 | return FAILURE 202 | 203 | time.sleep(settings.SEND_RECV_TIME) 204 | 205 | # 206 | # byte-based single send 207 | # 208 | else: 209 | mutant = misc.readBytes(settings.FUZZ_FILE) 210 | 211 | if(mutant == None): 212 | print("\n[ERROR] net.main() @ reading fuzz file: %s\n" % settings.FUZZ_FILE) 213 | return FAILURE 214 | 215 | # 216 | # helper for udp clients 217 | # 218 | # note: Resource Temporarily Unavailable error is OK (no data to recv) 219 | # 220 | if(config.prot == 'udp'): 221 | if((config.mode == settings.CLIENT) or (config.mode == settings.LOCAL_CLIENT)): 222 | try: 223 | (data, address) = config.conn.recvfrom(settings.RECV_SIZE) 224 | except Exception as error: 225 | if(config.debug): 226 | print("\n[INFO] net.main() @ udp recvfrom() failed: %s\n" % error) 227 | 228 | time.sleep(settings.SEND_RECV_TIME) # small delay 229 | 230 | # 231 | # crash scenario #2 232 | # 233 | # connect -> send (mutant) 234 | # 235 | # -> if send mutant fails, check for crash by repro'ing PREVIOUS fuzz file 236 | # 237 | # note: use send() not sendto() if already connected (UDP) 238 | # 239 | if(config.debug): 240 | print("\nsending data to target\n") 241 | 242 | try: 243 | if(config.prot == 'tcp'): 244 | config.conn.send(mutant) 245 | else: # udp is fun 246 | try: 247 | config.conn.sendto(mutant, (config.host, config.port)) 248 | except: 249 | config.conn.send(mutant) 250 | 251 | if(config.debug): 252 | print("sent to target:\n%s\n" % mutant) 253 | except Exception as error: 254 | if(config.debug): 255 | print("\n[INFO] net.main() @ send mutant: %s\n" % error) 256 | 257 | if(config.mode == settings.LOCAL_SERVER): 258 | if(misc.isUnix()): 259 | if(core.reproCrash(settings.FUZZ_FILE_PREV, cmdline, False) != SUCCESS): 260 | if(config.debug): 261 | print("[INFO] couldn't repro crash @ send(mutant)\n") 262 | return FAILURE 263 | else: 264 | return SUCCESS 265 | 266 | time.sleep(settings.SEND_RECV_TIME) # delay 267 | 268 | # 269 | # crash scenario #3 270 | # 271 | # connect -> send -> recv 272 | # 273 | # -> if recv fails or is zero bytes, check for crash by repro'ing CURRENT fuzz file 274 | # 275 | if((config.multibin == False) and (config.multistr == False)): 276 | data = None 277 | 278 | try: 279 | if(config.prot == 'tcp'): 280 | data = config.conn.recv(settings.RECV_SIZE) 281 | else: 282 | data = config.conn.recvfrom(settings.RECV_SIZE) 283 | 284 | if(config.debug): 285 | if(config.prot == 'tcp'): 286 | print("\nrecv from target:\n%s\n" % data) 287 | else: 288 | print("\nrecv from target:\n%s\n" % data[0]) 289 | # 290 | # unclear if even this is a reliable way to check if a UDP server went down 291 | # 292 | except ConnectionRefusedError as error: 293 | if((config.mode == settings.SERVER) or (config.mode == settings.LOCAL_SERVER)): 294 | if(config.prot == 'tcp'): 295 | if(config.debug): 296 | if(misc.isUnix()): 297 | print("\n[INFO] connection failed to %s -- check if the target crashed" % config.address) 298 | else: # win32 299 | print("\n[INFO] connection failed to %s -- can usually be ignored as its common on win32)" % config.address) 300 | else: # udp 301 | if(config.count == 1): 302 | print("[ERROR] connection failed to %s\n" % config.address) 303 | sys.exit(FAILURE) 304 | 305 | if((config.mode == settings.CLIENT) or (config.mode == settings.SERVER)): 306 | if((config.attach == None) and (config.report_crash == False)): 307 | if(misc.checkPort(config.prot, config.host, config.port) == False): 308 | config.down = True 309 | misc.clientServerCrash() 310 | return FAILURE 311 | 312 | return SUCCESS 313 | except Exception as error: 314 | if(config.debug): 315 | print("\n[INFO] net.main() @ recv from target: %s\n" % error) 316 | 317 | # 318 | # reproCrash() probably fails on UDP targets 319 | # 320 | if(config.mode == settings.LOCAL_SERVER): 321 | if(misc.isUnix()): 322 | if(core.reproCrash(settings.FUZZ_FILE_PREV, cmdline, False) != SUCCESS): 323 | if(config.debug): 324 | print("[INFO] couldn't repro crash @ send(mutant)\n") 325 | return FAILURE 326 | else: 327 | return SUCCESS 328 | 329 | if(data != None): 330 | if((len(data) == 0) and (config.mode == settings.LOCAL_SERVER)): 331 | if(misc.isUnix()): 332 | if(core.reproCrash(settings.FUZZ_FILE, cmdline, False) != SUCCESS): 333 | if(config.debug): 334 | print("[INFO] couldn't repro crash @ send(mutant)\n") 335 | return FAILURE 336 | else: 337 | return SUCCESS 338 | 339 | time.sleep(settings.SEND_RECV_TIME) # delay 340 | 341 | # 342 | # crash scenario #4 343 | # 344 | # connect -> send -> recv -> send (test) 345 | # 346 | # -> if send test data fails, check for crash by repro'ing CURRENT fuzz file 347 | # 348 | # note: do not send the test packet if we're fuzzing a session 349 | # 350 | if(settings.SEND_TEST_PACKET): 351 | if((config.multibin == False) and (config.multistr == False)): 352 | if((config.mode == settings.SERVER) or (config.mode == settings.LOCAL_SERVER)): 353 | if(config.debug): 354 | print("\nsending test data to target\n") 355 | 356 | try: 357 | if(config.prot == 'tcp'): 358 | config.conn.send(settings.TEST_DATA) 359 | else: 360 | config.conn.send(settings.TEST_DATA) 361 | 362 | if(config.debug): 363 | print("sent test data to target:\n%s\n" % settings.TEST_DATA) 364 | except Exception as error: 365 | if(config.debug): 366 | print("\n[INFO] net.main() @ send test: %s\n" % error) 367 | 368 | if(config.mode == settings.LOCAL_SERVER): 369 | if(misc.isUnix()): 370 | if(core.reproCrash(settings.FUZZ_FILE, cmdline, False) != SUCCESS): 371 | if(config.debug): 372 | print("[INFO] couldn't repro crash @ send(test)\n") 373 | return FAILURE 374 | else: 375 | return SUCCESS 376 | 377 | if(config.process != None): 378 | try: 379 | config.process.communicate(timeout=(config.maxtime * settings.NETWORK_TIMEOUT_MULTIPLE)) 380 | except Exception as error: 381 | if(config.debug): 382 | print("\n[INFO] net.main() @ process communicate: %s\n" % error) 383 | 384 | if(config.attach): 385 | if(triage.checkDebugger(cmdline) == SUCCESS): 386 | if(config.debug): 387 | print("triage.checkDebugger() found a crash\n") 388 | else: 389 | if(config.debug): 390 | print("triage.checkDebugger() did not find a crash\n") 391 | 392 | # 393 | # keep track of network execution times for stats 394 | # 395 | if(len(config.exec_times) <= settings.MAX_AVG_EXEC): 396 | config.exec_times.append(timer() - startTime) 397 | 398 | # 399 | # we don't want to check the return code if we're running inside a debugger 400 | # 401 | if((cmdline != None) and (config.process != None)): 402 | if((config.insulate == False) and (config.mode != settings.LOCAL_SERVER)): 403 | misc.checkForCrash(cmdline) 404 | 405 | return SUCCESS 406 | 407 | # 408 | # fuzz network servers 409 | # 410 | # takes care of setup for the connection, TLS, etc 411 | # 412 | def startClient(cmdline, replay): 413 | if(config.debug): 414 | print("entering net.startClient()\n") 415 | 416 | if(config.prot == 'tcp'): 417 | if(config.debug): 418 | print("starting tcp client\n") 419 | 420 | try: 421 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 422 | 423 | sock.setblocking(0) 424 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 425 | sock.settimeout(config.maxtime * settings.NETWORK_TIMEOUT_MULTIPLE) 426 | except Exception as error: 427 | print("\n[ERROR] net.startClient() @ tcp main socket: %s\n" % error) 428 | return FAILURE 429 | 430 | if(config.tls): 431 | try: 432 | conn = config.context.wrap_socket(sock) 433 | except Exception as error: 434 | print("\n[ERROR] socket SSL error: %s\n" % error) 435 | return FAILURE 436 | else: 437 | conn = sock 438 | 439 | config.conn = conn 440 | 441 | if(config.prot == 'udp'): 442 | if(config.debug): 443 | print("starting udp client\n") 444 | 445 | try: 446 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 447 | 448 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 449 | sock.settimeout(config.maxtime) 450 | 451 | if(misc.isUnix()): # win32 doesn't like this 452 | sock.setblocking(0) 453 | except Exception as error: 454 | print("\n[ERROR] net.startClient() @ udp main socket: %s\n" % error) 455 | return FAILURE 456 | 457 | config.conn = sock 458 | 459 | if(config.debug): 460 | print("started built-in client successfully") 461 | 462 | # 463 | # local client/server mode 464 | # 465 | if(config.mode == settings.LOCAL_SERVER): 466 | if(cmdline != None): 467 | if(replay): 468 | if(misc.isWin32()): 469 | thread = threading.Thread(target=run.main, args=(cmdline,)).start() 470 | else: 471 | if(debug.main(cmdline) != SUCCESS): 472 | if(config.debug): 473 | print("[ERROR] failed to start target server in debugger for replay") 474 | else: 475 | if(config.debug): 476 | print("started target server in debugger successfully") 477 | else: 478 | if(localNetRun(cmdline) != SUCCESS): 479 | if(config.debug): 480 | print("[ERROR] failed to start target server") 481 | else: 482 | if(config.debug): 483 | print("started target server successfully") 484 | else: 485 | print("[INFO] cmdline=None, did not start the target server\n") 486 | 487 | return SUCCESS 488 | 489 | # 490 | # fuzz network clients 491 | # 492 | # works, but probably could be better with threading, etc 493 | # 494 | def startServer(cmdline, replay): 495 | if(config.debug): 496 | print("entering startServer()\n") 497 | 498 | if(config.prot == 'tcp'): 499 | if(config.debug): 500 | print("starting tcp server\n") 501 | 502 | try: 503 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 504 | 505 | sock.setblocking(0) 506 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 507 | sock.settimeout(config.maxtime) 508 | 509 | sock.bind((config.host, config.port)) 510 | 511 | sock.listen(settings.TCP_BACKLOG) 512 | except Exception as error: 513 | print("\n[ERROR] net.startServer() @ tcp main socket: %s\n" % error) 514 | return FAILURE 515 | 516 | if(config.prot == 'udp'): 517 | if(config.debug): 518 | print("starting udp server\n") 519 | 520 | try: 521 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 522 | 523 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 524 | sock.settimeout(config.maxtime) 525 | 526 | if(misc.isUnix()): 527 | sock.setblocking(0) 528 | 529 | sock.bind((config.host, config.port)) 530 | except Exception as error: 531 | print("\n[ERROR] net.startServer() @ udp main socket: %s\n" % error) 532 | return FAILURE 533 | 534 | if((config.mode == settings.LOCAL_CLIENT) or (config.mode == settings.LOCAL_SERVER)): 535 | if(cmdline != None): 536 | if(replay): 537 | debug.main(cmdline) 538 | else: 539 | localNetRun(cmdline) 540 | else: 541 | if(config.count == 1): 542 | print("[+] waiting for %d seconds for remote target client setup...\n" % settings.CLIENT_TIMEOUT) 543 | time.sleep(settings.CLIENT_TIMEOUT) 544 | 545 | if(config.debug): 546 | print("started target client successfully") 547 | 548 | # 549 | # network GUI clients may need this 550 | # 551 | if(config.key != None): 552 | misc.hitKey() 553 | 554 | if(config.prot == 'tcp'): 555 | config.cli_conn = None 556 | 557 | try: 558 | (ssock, config.cli_conn) = sock.accept() 559 | 560 | ssock.setblocking(0) 561 | ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 562 | ssock.settimeout(config.maxtime) 563 | 564 | if(config.tls): 565 | try: 566 | conn = config.context.wrap_socket(ssock, server_side=True) 567 | except Exception as error: 568 | print("\n[ERROR] socket SSL error: %s\n" % error) 569 | return FAILURE 570 | else: 571 | conn = ssock 572 | except socket.timeout: 573 | if(config.debug): 574 | print("\n[INFO] timed out waiting for client connection: %s:%d\n" % (config.host, config.port)) 575 | return FAILURE 576 | 577 | config.conn = conn 578 | 579 | if(config.debug): 580 | print("started built-in server successfully") 581 | 582 | if(config.cli_conn != None): 583 | if(config.debug): 584 | print("connection from %s\n" % config.cli_conn[0]) 585 | 586 | sock.close() 587 | 588 | if(config.prot == 'udp'): 589 | config.conn = sock 590 | 591 | try: 592 | (data, address) = sock.recvfrom(settings.RECV_SIZE) 593 | except Exception as error: 594 | print("\n[INFO] net.startServer() @ udp recvfrom() failed: %s\n" % error) 595 | return FAILURE 596 | 597 | return SUCCESS 598 | 599 | # 600 | # local client/server mode 601 | # 602 | # only run target app once unless we are restarting it (eg. after a crash) 603 | # 604 | def localNetRun(cmdline): 605 | if(config.debug): 606 | print("entering localNetRun()\n") 607 | 608 | # 609 | # run insulated GUI network apps and network servers in a debugger 610 | # 611 | if(config.insulate or (config.mode == settings.LOCAL_SERVER)): 612 | if(config.count == 1): 613 | if(misc.isUnix()): 614 | debug.main(cmdline) 615 | else: # win32 616 | # 617 | # we use "this one cool trick" to make remote fuzzing work on win32 as on *nix, we 618 | # start just start the process or debugger and go, but on win32 we're using the 619 | # winappdbg engine which is blocking and times out before we hit our network code 620 | # to continue -- so we just create a thread and let it them run in bliss and harmony 621 | # 622 | thread = threading.Thread(target=run.main, args=(cmdline,)).start() 623 | 624 | # 625 | # give the user time to setup the interactive app 626 | # 627 | if(config.insulate): 628 | misc.doInsulate() 629 | # 630 | # local network clients 631 | # 632 | else: 633 | if(misc.isWin32()): 634 | thread = threading.Thread(target=run.main, args=(cmdline,)).start() 635 | else: 636 | run.main(cmdline) 637 | 638 | return SUCCESS 639 | 640 | # 641 | # replay crashes for confirmation 642 | # 643 | # core.repro() vs net.replay() 644 | # 645 | # - repro() tries to fully setup the environment and replay the crash for 646 | # both local and network apps and calls replay() to handle network stuff 647 | # 648 | # - replay() is similar, but only handles the replay of network crashes 649 | # 650 | def replay(cmdline): 651 | if(config.debug): 652 | print("entering net.replay()\n") 653 | 654 | # print("replay_file=%s\n" % replay_file) 655 | 656 | if((config.mode == settings.CLIENT) or (config.mode == settings.LOCAL_CLIENT)): 657 | if(misc.isWin32()): 658 | startServerWin32(cmdline, replay) # win32 version 659 | else: 660 | startServer(cmdline, replay) # start built-in server to fuzz target client 661 | else: 662 | startClient(cmdline, replay) # start built-in client to fuzz target server 663 | 664 | if(config.conn == None): 665 | if(config.mode == settings.CLIENT): 666 | print("[ERROR] no connection from the client\n") 667 | else: 668 | print("[ERROR] net.replay() @ socket: no valid connection socket\n") 669 | misc.killTargetProcesses() 670 | sys.exit(FAILURE) 671 | 672 | if(config.mode == settings.SERVER or (config.mode == settings.LOCAL_SERVER)): 673 | try: 674 | config.conn.connect((config.host, config.port)) 675 | except Exception as error: 676 | if(config.debug): 677 | print("\n[ERROR] net.replay() @ connect: %s\n" % error) 678 | return FAILURE 679 | 680 | # 681 | # initial replay recv() 682 | # 683 | buf = None 684 | 685 | try: 686 | if(config.prot == 'tcp'): 687 | buf = config.conn.recv(settings.RECV_SIZE) 688 | else: 689 | (buf, address) = config.conn.recvfrom(settings.RECV_SIZE) 690 | except Exception as error: 691 | if(config.debug): 692 | print("\n[INFO] net.main() @ initial recv(): %s\n" % error) 693 | 694 | if(config.debug): 695 | if(buf): 696 | print("recv from target:\n%s\n" % buf) 697 | 698 | if(config.multibin or config.multistr): 699 | if(len(config.session) == 0): 700 | print("\n[ERROR] no session data to replay, check inputs\n") 701 | return FAILURE 702 | 703 | for s in config.session: 704 | if(config.multibin): 705 | sb = s # multibin is already bytes 706 | else: 707 | sb = bytearray(s.encode()) 708 | 709 | if(misc.sendRecvBytes(sb) != SUCCESS): 710 | return FAILURE 711 | 712 | time.sleep(settings.SEND_RECV_TIME) 713 | 714 | else: 715 | # 716 | # get mutant from file 717 | # 718 | mutant = misc.readBytes(config.replay_file) 719 | 720 | if(mutant == None): 721 | print("\n[ERROR] net.replay() @ reading replay file: %s\n" % config.replay_file) 722 | return FAILURE 723 | 724 | # 725 | # do recvfrom() first if udp 726 | # 727 | if(config.prot == 'udp'): 728 | try: 729 | (data, address) = config.conn.recvfrom(settings.RECV_SIZE) 730 | except Exception as error: 731 | if(config.debug): 732 | print("\n[INFO] net.replay() @ udp recvfrom() failed: %s\n" % error) 733 | return FAILURE 734 | 735 | # 736 | # send data 737 | # 738 | if(config.debug): 739 | print("\nsending data to target\n") 740 | 741 | try: 742 | config.conn.send(mutant) 743 | 744 | if(config.debug): 745 | print("sent to target:\n%s\n" % mutant) 746 | except Exception as error: 747 | if(config.debug): 748 | print("\n[INFO] net.replay() @ send mutant: %s\n" % error) 749 | return FAILURE 750 | 751 | # 752 | # recv data 753 | # 754 | # note: only do this for non-session replays as we already recv() in misc.sendRecvBytes() 755 | # 756 | if((config.multibin == False) and (config.multistr == False)): 757 | try: 758 | if(config.prot == 'tcp'): 759 | data = config.conn.recv(settings.RECV_SIZE) 760 | else: 761 | data = config.conn.recvfrom(settings.RECV_SIZE) 762 | 763 | if(config.debug): 764 | print("\nrecv from target:\n%s\n" % data) 765 | except Exception as error: 766 | if(config.debug): 767 | print("\n[INFO] net.replay() @ recv from target: %s\n" % error) 768 | return FAILURE 769 | 770 | if((len(data) == 0) and (config.mode == settings.LOCAL_SERVER)): 771 | return FAILURE 772 | 773 | # 774 | # test send 775 | # 776 | if(settings.SEND_TEST_PACKET): 777 | if((config.mode == settings.SERVER) or (config.mode == settings.LOCAL_SERVER)): 778 | if(config.debug): 779 | print("\nsending test data to target\n") 780 | 781 | try: 782 | config.conn.send(settings.TEST_DATA) 783 | 784 | if(config.debug): 785 | print("sent test data to target:\n%s\n" % settings.TEST_DATA) 786 | except Exception as error: 787 | if(config.debug): 788 | print("\n[INFO] net.replay() @ send test data: %s\n" % error) 789 | return FAILURE 790 | 791 | if(config.process != None): 792 | try: 793 | config.process.communicate(timeout=(config.maxtime * settings.NETWORK_TIMEOUT_MULTIPLE)) 794 | except Exception as error: 795 | if(config.debug): 796 | print("\n[INFO] net.replay() @ process communicate: %s\n" % error) 797 | 798 | # 799 | # we don't want to check the return code if we're running inside a debugger 800 | # 801 | if((cmdline != None) and (config.process != None)): 802 | if((config.insulate == False) and (config.mode != settings.LOCAL_SERVER)): 803 | if(config.debug): 804 | print("net.replay() calling checkForCrash()\n") 805 | 806 | misc.checkForCrash(cmdline) 807 | 808 | return SUCCESS 809 | -------------------------------------------------------------------------------- /requirements/requirements-py2.txt: -------------------------------------------------------------------------------- 1 | future 2 | psutil 3 | pyautogui 4 | pytest 5 | tqdm 6 | subprocess32 7 | winappdbg -------------------------------------------------------------------------------- /requirements/requirements-py3.txt: -------------------------------------------------------------------------------- 1 | future 2 | psutil 3 | pytest 4 | tqdm 5 | subprocess32 6 | winappdbg 7 | pyautogui -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # run.py 7 | # 8 | 9 | import os 10 | import sys 11 | import signal 12 | import shutil 13 | import subprocess32 as subprocess 14 | import time 15 | from time import time as timer 16 | 17 | if(str(sys.platform).startswith('win32')): 18 | from winappdbg import * 19 | 20 | try: 21 | FileNotFoundError # py3 22 | except NameError: 23 | FileNotFoundError = IOError # py2 24 | 25 | import triage 26 | import debug 27 | import misc 28 | import config 29 | import settings 30 | from settings import SUCCESS, FAILURE 31 | 32 | # 33 | # choose your own adventure 34 | # 35 | def main(cmdline): 36 | if(misc.isUnix()): 37 | return unix(cmdline) 38 | elif(misc.isWin32()): 39 | return windows(cmdline) 40 | else: 41 | return FAILURE 42 | 43 | # 44 | # Linux/Mac binary execution support 45 | # 46 | def unix(cmdline): 47 | if(config.debug): 48 | print("current_input: %s\n" % config.current_input) 49 | print("cmdline: %s\n" % cmdline) 50 | 51 | if(config.current_input == None): 52 | print("[ERROR] current_input cannot be None\n") 53 | return FAILURE 54 | 55 | if(settings.KILL_EXISTING_PROCESS): 56 | if(config.debug): 57 | print("killing any running processes named %s before running a new one\n" % config.target) 58 | 59 | misc.killProcessByName(config.target) 60 | 61 | startTime = timer() 62 | 63 | if(config.debug): 64 | print("\n[INFO] unix.run() @ starting target process: %s\n" % cmdline) 65 | 66 | # 67 | # capture stdout/stderr in a file 68 | # 69 | try: 70 | with open(settings.FUZZ_OUTPUT, 'w') as file: 71 | if('FUZZ' in cmdline): 72 | process = subprocess.Popen(cmdline, 73 | stdin=None, 74 | stdout=file, 75 | stderr=file, 76 | preexec_fn=os.setsid, 77 | env=config.env) 78 | else: 79 | process = subprocess.Popen(cmdline, 80 | stdin=open(config.current_input), 81 | stdout=file, 82 | stderr=file, 83 | preexec_fn=os.setsid, 84 | env=config.env) 85 | 86 | if(config.debug): 87 | print("unix.run() %s started @ pid=%d\n" % (cmdline, process.pid)) 88 | 89 | # 90 | # do not timeout insulated apps (interactive apps) 91 | # and use sleep() instead of a process timeout if 92 | # client or server fuzzing is in progress 93 | # 94 | if(config.insulate == False): 95 | if(config.mode == settings.LOCAL): 96 | (output, error) = process.communicate(timeout=config.maxtime) 97 | 98 | if(error): 99 | print("[ERROR] '%s' @ pid=%d: %s\n" % (cmdline, process.pid(), error)) 100 | else: 101 | time.sleep(config.maxtime) 102 | except subprocess.TimeoutExpired as error: 103 | if(config.debug): 104 | print("%s\n" % error) 105 | 106 | misc.killProcess(process.pid) 107 | except FileNotFoundError as error: 108 | print("[ERROR] run.unix() @ main run cmdline: %s" % error) 109 | sys.exit(FAILURE) 110 | except IOError as error: 111 | print("[INFO] run.unix() @ write(FUZZ_OUTPUT): %s" % error) 112 | return FAILURE 113 | except Exception as error: 114 | print("[ERROR] run.unix() failed: %s" % error) 115 | return FAILURE 116 | 117 | config.process = process 118 | 119 | # 120 | # keep track of execution times for stats 121 | # 122 | if(len(config.exec_times) <= settings.MAX_AVG_EXEC): 123 | config.exec_times.append(timer() - startTime) 124 | 125 | # 126 | # only do this for local apps in run.unix() because local 127 | # client and server runs call this in their own functions 128 | # because the process may not finish until after connection 129 | # 130 | if(config.mode == settings.LOCAL): 131 | misc.checkForCrash(cmdline) 132 | 133 | return SUCCESS 134 | 135 | # 136 | # make sure each process properly goes away after timeout 137 | # 138 | def win32KillAll(dbg): 139 | if(config.debug): 140 | print("entering run.win32KillAll()\n") 141 | 142 | for pid in dbg.get_debugee_pids(): 143 | try: 144 | dbg.detach(pid) 145 | dbg.kill(pid) 146 | except: 147 | misc.killProcess(pid) # last ditch effort 148 | # pass 149 | 150 | # 151 | # Win32 binary execution support 152 | # 153 | # (based on https://winappdbg.readthedocs.io/en/latest/MoreExamples.html) 154 | # 155 | # note: stdin fuzzing not supported on win32 156 | # 157 | def windows(cmdline): 158 | if(config.debug): 159 | print("entering run.windows() with cmdline %s\n" % cmdline) 160 | 161 | if(settings.KILL_EXISTING_PROCESS): 162 | if(config.debug): 163 | print("killing any running processes named %s before running a new one\n" % config.target) 164 | 165 | misc.killProcessByName(config.target) 166 | 167 | with Debug(debug.win32CrashHandler, bKillOnExit = True) as dbg: 168 | try: 169 | dbg.execv(cmdline) 170 | System.set_kill_on_exit_mode(True) 171 | except Exception as error: 172 | print("\n[ERROR] run.windows() @ main run: %s\n" % error) 173 | sys.exit(FAILURE) 174 | 175 | config.dbg32 = dbg # so we can kill the eg. local server process as needed 176 | 177 | # 178 | # let local network servers join the '9 club 179 | # 180 | if(config.mode == settings.LOCAL_SERVER): 181 | maxtime = 9999999 # don't stop, running 182 | else: 183 | maxtime = config.maxtime 184 | 185 | timeout = timer() + maxtime 186 | 187 | while(dbg and timer() < timeout): 188 | try: 189 | dbg.wait(1000) 190 | except: 191 | continue 192 | 193 | try: 194 | try: 195 | dbg.dispatch() 196 | except Exception as error: 197 | if(config.debug): 198 | print("[INFO] run.windows() @ dbg dispatch: %s" % error) 199 | finally: 200 | try: 201 | dbg.cont() 202 | except Exception as error: 203 | if(config.debug): 204 | print("[INFO] run.windows() @ dbg continue: %s" % error) 205 | 206 | try: 207 | win32KillAll(dbg) 208 | except Exception as error: 209 | if(config.debug): 210 | print("[INFO] run.windows() @ dbg continue: %s" % error) 211 | # pass 212 | 213 | return SUCCESS 214 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # settings.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import warnings 13 | warnings.filterwarnings("ignore") 14 | 15 | import config 16 | 17 | # 18 | # status 19 | # 20 | SUCCESS = 0 21 | FAILURE = 1 22 | 23 | # 24 | # runtime config 25 | # 26 | if(str(sys.platform).startswith('linux') or 27 | str(sys.platform).startswith('darwin')): 28 | TMP_DIR = '/tmp/litefuzz' 29 | if(str(sys.platform).startswith('win32')): 30 | TMP_DIR = 'C:\\Windows\\Temp\\litefuzz' 31 | 32 | RUN_DIR = TMP_DIR # this gets set for each fuzzing session's unique run dir 33 | 34 | CRASH_DIR = 'crashes' 35 | CHECK_DUPS_PREV_RUN = True 36 | TIMEOUT = 10 37 | 38 | # 39 | # primary modes 40 | # 41 | LOCAL = 1 42 | LOCAL_CLIENT = 2 43 | LOCAL_SERVER = 3 44 | CLIENT = 4 45 | SERVER = 5 46 | 47 | # 48 | # set based on TMP_DIR and the generated per-run id 49 | # 50 | FUZZ_FILE = None 51 | FUZZ_FILE_PREV = None 52 | FUZZ_OUTPUT = None 53 | FUZZ_INFO = None 54 | FUZZ_INFO_STATIC = None 55 | FUZZ_DIFF = None 56 | FUZZ_DIFF_STRING = None 57 | FUZZ_DIFF_ORIG = None 58 | FUZZ_DIFF_FUZZ = None 59 | FUZZ_OUTPUT_DEBUG = None 60 | FUZZ_OUTPUTS_DEBUG = None 61 | 62 | MIN_FILE = None 63 | MIN_FILE_ORIG = None 64 | 65 | RUN_ID_MIN = 1000 66 | RUN_ID_MAX = 9999 67 | 68 | # 69 | # mutators 70 | # 71 | MUTATOR_CHOICE = 0 # random 72 | 73 | if((str(sys.platform).startswith('linux')) and (sys.version_info[0] >= 3)): 74 | MUTATOR_MAX = 7 # pyradamsa is linux only 75 | else: 76 | MUTATOR_MAX = 6 77 | 78 | FLIP_MUTATOR = 1 79 | HIGHLOW_MUTATOR = 2 80 | INSERT_MUTATOR = 3 81 | REMOVE_MUTATOR = 4 82 | CARVE_MUTATOR = 5 83 | OVERWRITE_MUTATOR = 6 84 | RADAMSA_MUTATOR = 7 85 | 86 | FLIP_MUTATOR_ENABLE = True 87 | HIGHLOW_MUTATOR_ENABLE = True 88 | INSERT_MUTATOR_ENABLE = True 89 | REMOVE_MUTATOR_ENABLE = True 90 | CARVE_MUTATOR_ENABLE = True 91 | OVERWRITE_MUTATOR_ENABLE = True 92 | RADAMSA_MUTATOR_ENABLE = True 93 | 94 | # number of random bytes to flip for each mutated test case (simple mutator) 95 | MUTATION_MIN = 1 96 | MUTATION_MAX = 8 97 | 98 | # insert mutator 99 | SIZE_MIN = 1 100 | SIZE_MAX = 5000 101 | 102 | # 103 | # unix crash codes 104 | # 105 | SIGTRAP = -5 106 | SIGABRT = -6 107 | SIGILL = -7 108 | SIGFPE = -8 109 | SIGSEGV = -11 110 | 111 | # 112 | # golang 113 | # 114 | SIGGO = 2 115 | 116 | EXCEPTIONS = { 117 | 'EXC_BREAKPOINT' : SIGTRAP, 118 | 'EXC_ARITHMETIC' : SIGFPE, 119 | 'EXC_BAD_INSTRUCTION' : SIGILL, 120 | 'EXC_BAD_ACCESS' : SIGSEGV, 121 | 'SIGTRAP' : SIGTRAP, 122 | 'SIGABRT' : SIGABRT, 123 | 'SIGFPE' : SIGFPE, 124 | 'SIGILL' : SIGILL, 125 | 'SIGSEGV' : SIGSEGV 126 | } 127 | 128 | # misc 129 | ARTIFACTS_ENABLE = True 130 | DIFF_ENABLE = True 131 | KEEPAWAKE_ENABLE = True 132 | KILL_EXISTING_PROCESS = True 133 | 134 | # timeouts 135 | INSULATE_TIMEOUT = 30 136 | EXEC_TIMEOUT = 3 137 | TOOL_TIMEOUT = 5 138 | DEBUG_TIMEOUT_MULTIPLE = 10 139 | NETWORK_TIMEOUT_MULTIPLE = 2 140 | CLIENT_TIMEOUT = 10 141 | LOCAL_SERVER_TIMEOUT = 5 142 | 143 | if(str(sys.platform).startswith('win32')): 144 | null = 'NUL' 145 | else: 146 | null = '/dev/null' 147 | 148 | # mac 149 | KYA_BIN = '/Applications/KeepingYouAwake.app/Contents/MacOS/KeepingYouAwake' 150 | KYA_NAME = 'KeepingYouAwake' 151 | 152 | REPORT_CRASH_NAME = 'ReportCrash' 153 | # REPORT_CRASH_DIR = os.environ["HOME"] + '/Library/Logs/DiagnosticReports' 154 | REPORT_CRASH_DIR = '/Library/Logs/DiagnosticReports' 155 | REPORT_CRASH_DIR_OLD = REPORT_CRASH_DIR + os.sep + 'OLD' 156 | REPORT_CRASH_LOAD = 'sudo launchctl load -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist' 157 | # REPORT_CRASH_LOAD_ROOT = 'sudo launchctl load -w /System/Library/LaunchAgents/com.apple.ReportCrash.Root.plist' 158 | 159 | # windows 160 | DIFF_WIN_BIN = 'C:\\ProgramData\\chocolatey\\lib\\diffutils\\tools\\bin\\diff.exe' 161 | OD_WIN_BIN = 'C:\\Program Files (x86)\\GnuWin32\\bin\\od.exe' 162 | 163 | CONSOLE_DEBUGGER_PATH = 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\cdb.exe' 164 | MEMORY_DUMP_REG_KEY = 'SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\LocalDumps\\' 165 | 166 | MEMORY_DUMP = False 167 | 168 | # malloc helpers 169 | LIBEFENCE_PATH = '/usr/lib/libefence.so' 170 | LIBGMALLOC_PATH = '/usr/lib/libgmalloc.dylib' 171 | 172 | GLIBC_MALLOC_CHECK = 'MALLOC_CHECK_=3' 173 | 174 | DEBUG_DISSASSEMBLE_LLDB = 'dis -s $pc-32 -c 24 -m -F intel' 175 | 176 | DEBUG_ENV_EFENCE_GLIBC_ENABLE = False # main 177 | DEBUG_ENV_EFENCE_ENABLE = False 178 | DEBUG_ENV_GLIBC_ENABLE = False 179 | 180 | DEBUG_ENV_EFENCE = dict(os.environ, LD_PRELOAD=LIBEFENCE_PATH) 181 | DEBUG_ENV_GLIBC = dict(os.environ, MALLOC_CHECK_='3') # fallback 182 | 183 | DEBUG_ENV_GMALLOC_ENABLE = False 184 | DEBUG_ENV_GMALLOC = dict(os.environ, DYLD_INSERT_LIBRARIES=LIBGMALLOC_PATH) 185 | 186 | DEBUG_ENV_PAGEHEAP_ENABLE = False 187 | DEBUG_ENV_PAGEHEAP_DISABLE = False 188 | DEBUG_ENV_GFLAG_MAGIC = '0x02000000' 189 | DEBUG_ENV_PAGEHEAP_MAGIC = '0x3' 190 | 191 | GFLAGS_BIN_PATH = 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\gflags.exe' 192 | PAGEHEAP_REG_KEY = 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\' 193 | 194 | USER_VA_MAX = '0x7fffffffffff' 195 | SIMILAR_PC_RANGE = '0x1000' 196 | 197 | # 198 | # defaults 199 | # 200 | ITERATIONS_DEFAULT = 1 201 | MAX_AVG_EXEC = 100 202 | MAX_TIME_DEFAULT = 1 203 | BIG_INPUT_SIZE = 1000000 # 1mb 204 | MAX_INPUT_SIZE = 10000000 # 10mb limit 205 | NET_SLEEP_TIME = 20 206 | SEND_RECV_TIME = 0.1 207 | 208 | # 209 | # network 210 | # 211 | RECV_SIZE = 4096 212 | TCP_BACKLOG = 5 213 | TLS_DIR = 'tls' + os.sep 214 | TEST_DATA = b'test' 215 | NETWORK_CRT = TLS_DIR + 'network.crt' 216 | NETWORK_KEY = TLS_DIR + 'network.pem' 217 | GENERATE_CERT_CMD = 'openssl req -x509 -new -nodes -subj \'/O=o/C=CC/CN=NC\' -keyout ' + NETWORK_KEY + ' -out ' + NETWORK_CRT + ' -days 5555' 218 | SEND_TEST_PACKET = False 219 | TCP_KEEP_GOING = True 220 | -------------------------------------------------------------------------------- /setup/linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # linux.sh 4 | # 5 | # litefuzz project 6 | # 7 | # setup and install deps on Ubuntu Linux (22.04 tested + Py3) 8 | # 9 | # note: run as user with sudo privileges and in the litefuzz root directory 10 | # 11 | 12 | echo -e "\ninstalling litefuzz deps and setup on Ubuntu Linux..." 13 | 14 | echo -e '\n> installing apt packages, enter the password for sudo if prompted\n' 15 | 16 | sudo apt update 17 | sudo apt install -y build-essential gnome-devel gcc gdb libgtk-3-dev python3 python3-dev python3-pip python3-tk python-tk electric-fence 18 | 19 | echo -e '\n> grabbing !exploitable for gdb\n' 20 | 21 | git clone https://github.com/jfoote/exploitable 22 | pushd exploitable 23 | sudo python3 setup.py install 24 | popd 25 | 26 | # 27 | # make exploitable autoload in gdb 28 | # 29 | EXPLOITABLE_PY=$(sudo find /usr/local/lib -name exploitable.py) 30 | echo "source $EXPLOITABLE_PY" >> ~/.gdbinit 31 | 32 | echo -e '\n> installing python packages and setting py3 as the default python\n' 33 | 34 | pip3 install -r requirements/requirements-py3.txt 35 | pip3 install pyradamsa # didn't include in requirements as its a Linux only package 36 | 37 | sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1 38 | 39 | echo -e '\n> making test crash apps\n' 40 | 41 | pushd test/linux 42 | make 43 | popd 44 | 45 | chmod +x litefuzz.py 46 | 47 | echo -e '\nfinished!\n' 48 | -------------------------------------------------------------------------------- /setup/mac-xcode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # mac-xcode.sh 4 | # 5 | # full credit and source: https://github.com/timsutton/osx-vm-templates/blob/master/scripts/xcode-cli-tools.sh 6 | # 7 | # catalina fix from: https://apple.stackexchange.com/questions/107307/how-can-i-install-the-command-line-tools-completely-from-the-command-line#comment553658_195963 8 | # 9 | # updated first check to support OS X 11+ 10 | # 11 | 12 | if [[ ! "$INSTALL_XCODE_CLI_TOOLS" =~ ^(true|yes|on|1|TRUE|YES|ON])$ ]]; then 13 | exit 14 | fi 15 | 16 | # Get and install Xcode CLI tools 17 | OSX_BIG_VERS=$(sw_vers -productVersion | awk -F "." '{print $1}') 18 | OSX_VERS=$(sw_vers -productVersion | awk -F "." '{print $2}') 19 | 20 | # on 10.9+, we can leverage SUS to get the latest CLI tools 21 | if [ "$OSX_VERS" -ge 9 ] || [ "$OSX_BIG_VERS" -ge 11 ]; then 22 | # create the placeholder file that's checked by CLI updates' .dist code 23 | # in Apple's SUS catalog 24 | touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress 25 | # find the CLI Tools update 26 | #PROD=$(softwareupdate -l | grep "\*.*Command Line" | tail -n 1 | awk -F"*" '{print $2}' | sed -e 's/^ *//' | tr -d '\n') 27 | PROD=$(softwareupdate -l | grep "*.*Command Line" | tail -n 1 | awk -F"*" '{print $2}' | sed -e 's/^ *//' | sed 's/Label: //g' | tr -d '\n') 28 | # install it 29 | softwareupdate -i "$PROD" --verbose 30 | rm /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress 31 | 32 | # on 10.7/10.8, we instead download from public download URLs, which can be found in 33 | # the dvtdownloadableindex: 34 | # https://devimages.apple.com.edgekey.net/downloads/xcode/simulators/index-3905972D-B609-49CE-8D06-51ADC78E07BC.dvtdownloadableindex 35 | else 36 | [ "$OSX_VERS" -eq 7 ] && DMGURL=http://devimages.apple.com.edgekey.net/downloads/xcode/command_line_tools_for_xcode_os_x_lion_april_2013.dmg 37 | [ "$OSX_VERS" -eq 8 ] && DMGURL=http://devimages.apple.com.edgekey.net/downloads/xcode/command_line_tools_for_osx_mountain_lion_april_2014.dmg 38 | 39 | TOOLS=clitools.dmg 40 | curl "$DMGURL" -o "$TOOLS" 41 | TMPMOUNT=`/usr/bin/mktemp -d /tmp/clitools.XXXX` 42 | hdiutil attach "$TOOLS" -mountpoint "$TMPMOUNT" 43 | if [ "$OSX_VERS" -eq 7 ]; then 44 | # using '-allowUntrusted' because Lion CLI tools are so old Apple never built another 45 | # package that doesn't have an expired CA cert. (Expired February 15, 2015) 46 | installer -pkg "$(find $TMPMOUNT -name '*.mpkg')" -allowUntrusted -target / 47 | else 48 | installer -pkg "$(find $TMPMOUNT -name '*.mpkg')" -target / 49 | fi 50 | hdiutil detach "$TMPMOUNT" 51 | rm -rf "$TMPMOUNT" 52 | rm "$TOOLS" 53 | exit 54 | fi -------------------------------------------------------------------------------- /setup/mac.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # mac.sh 4 | # 5 | # litefuzz project 6 | # 7 | # setup and install deps on Mac OS X (11 and 12 tested) 8 | # 9 | # note: run as user with sudo privileges and in the litefuzz root directory 10 | # 11 | 12 | echo -e '\ninstalling litefuzz deps and setup on Mac...' 13 | 14 | # 15 | # setup sudo user and groups 16 | # 17 | # note: nopasswd sudo is technically optional, but it allows us to sudo unattended for the rest of 18 | # the setup and will probably come in handy later eg. if fuzzing clients that talk to privileged ports 19 | # 20 | echo -e '\n> configuring nopasswd sudo + dev groups for the current user, so enter your password if prompted\n' 21 | 22 | echo "$(whoami) ALL=(ALL) NOPASSWD: ALL" | (sudo EDITOR="tee -a" visudo) 23 | 24 | sudo dseditgroup -o edit -a $(whoami) -t user admin 25 | sudo dseditgroup -o edit -a $(whoami) -t user _developer 26 | 27 | # 28 | # install xcode and enable developer mode 29 | # 30 | # note: if xcode setup fails with NSURLErrorDomain error, check your network connection 31 | # 32 | echo -e '\n> installing xcode... hang tight as the download may take a while\n' 33 | 34 | chmod +x setup/mac-xcode.sh 35 | INSTALL_XCODE_CLI_TOOLS=true sudo -E setup/mac-xcode.sh 36 | 37 | sudo DevToolsSecurity -enable 38 | 39 | # 40 | # python2 pip (py2 is recommended) 41 | # 42 | echo -e '\n> installing py2\n' 43 | 44 | curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py 45 | python get-pip.py 46 | rm get-pip.py 47 | 48 | # export PATH=$PATH:/Users/$(whoami)/Library/Python/2.7/bin 49 | 50 | echo -e '\n> installing python packages\n' 51 | 52 | pip install -r requirements/requirements-py2.txt --user 53 | pip3 install -r requirements/requirements-py3.txt --user 54 | 55 | # 56 | # disable ReportCrash 57 | # 58 | # note: enable it later if you intend on using the --reportcrash feature 59 | # 60 | echo -e '\n> disabling ReportCrash and friends\n' 61 | 62 | launchctl unload -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist 63 | sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.ReportCrash.Root.plist 64 | 65 | # 66 | # disable computer sleeping so it doesn't interfere with the fuzzing process 67 | # 68 | echo -e '\n> disabling system sleep modes' 69 | 70 | # sudo pmset -a sleep 0 71 | # sudo pmset -a disksleep 0 72 | sudo systemsetup -setsleep Never 73 | 74 | # 75 | # try to avoid SSH timeouts 76 | # 77 | echo -e '\n> reconfiguring SSH' 78 | 79 | # sudo echo -e 'TCPKeepAlive yes\nClientAliveInterval 0\nClientAliveMax 0' >> /etc/ssh/sshd_config 80 | sudo bash -c "echo -e 'TCPKeepAlive yes\nClientAliveInterval 0\nClientAliveMax 0' >> sshd_config" 81 | sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist 82 | sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist 83 | 84 | # 85 | # brew.sh 86 | # 87 | echo -e '\n> installing brew and packages\n' 88 | 89 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" making test crash apps\n' 99 | 100 | pushd test/mac 101 | make 102 | popd 103 | 104 | chmod +x litefuzz.py 105 | 106 | echo -e '\nfinished!\n' 107 | echo -e 'note: if you see errors with the xcode install, try and run the xcode script directly' 108 | 109 | # 110 | # disable SIP so we can do auto-triage upon crashes 111 | # 112 | # (lldb) run ... 113 | # error: process exited with status -1 (this is a non-interactive debug session, cannot get permission to debug processes.) 114 | # 115 | echo -e '\nok, just one last thing: you need to boot into recovery mode and disable SIP so that debugging actually works\n' 116 | echo -e '1.1) For bare metal, reboot and hold down Command + R until you see the Apple logo or spinning thing' 117 | echo -e '1.2) For virtual machines, search for instructions specific to OS X or OS 11, Temporary Installation Source Disk, Restart to Firmware, etc' 118 | echo -e '2) At the top menu bar select Utilities -> Terminal' 119 | echo -e '3) csrutil disable and then reboot\n' 120 | -------------------------------------------------------------------------------- /setup/windows.bat: -------------------------------------------------------------------------------- 1 | :: 2 | :: windows.bat 3 | :: 4 | :: litefuzz project 5 | :: 6 | :: setup and install deps on Windows (10 tested + Py2) 7 | :: 8 | :: note: run as Administrator and in the litefuzz root directory 9 | :: 10 | 11 | @echo off 12 | 13 | echo installing litefuzz deps and setup on Windows... 14 | echo. 15 | 16 | :: 17 | :: install chocolatey package manager 18 | :: 19 | :: source: https://stackoverflow.com/questions/52578270/install-python-with-cmd-or-powershell 20 | :: 21 | 22 | echo ^> fetching chocolatey package manager 23 | echo. 24 | 25 | @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" 26 | 27 | echo. 28 | echo ^> installing choco packages 29 | 30 | choco install -y python2 gnuwin32-coreutils.install diffutils gsudo make mingw openssl windows-sdk-10-version-2004-windbg 31 | 32 | echo. 33 | echo ^> installing python dependencies 34 | 35 | C:\Python27\Scripts\pip.exe install -r requirements\requirements-py2.txt 36 | 37 | echo. 38 | echo ^> disabling windows error reporting 39 | 40 | @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "Disable-WindowsErrorReporting" 41 | 42 | echo. 43 | echo ^> making test crash apps 44 | 45 | cd test\windows 46 | make 47 | cd ..\.. 48 | 49 | echo. 50 | echo finished! 51 | -------------------------------------------------------------------------------- /test/a.c: -------------------------------------------------------------------------------- 1 | /* 2 | crash test app #1 3 | 4 | null ptr deref 5 | */ 6 | #include 7 | 8 | int main() { 9 | *((int *)0) = 0; 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /test/b.c: -------------------------------------------------------------------------------- 1 | /* 2 | test crash app #2 3 | 4 | div-by-zero 5 | 6 | note: doesn't crash on mac, but exits code 160 7 | */ 8 | 9 | #include 10 | 11 | int main() { 12 | return 1 / 0; 13 | } 14 | -------------------------------------------------------------------------------- /test/c.c: -------------------------------------------------------------------------------- 1 | /* 2 | crash test app #3 3 | 4 | heap corruption 5 | 6 | OS-specific notes to catch the crash 7 | 8 | linux: LD_PRELOAD=/usr/lib/libefence.so ./c 9 | mac: DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib ./c 10 | windows: turn on pageheap 11 | */ 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | int main() { 18 | char *buf = malloc(32); 19 | memset(buf, 'B', sizeof(buf) + 26); 20 | free(buf); 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /test/d-gui.c: -------------------------------------------------------------------------------- 1 | /* 2 | test crash app #4 3 | 4 | format string leading to invalid read in a GUI 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | int main(int argc, char *argv[]) { 12 | gtk_init(&argc, &argv); 13 | 14 | char *data; 15 | 16 | if(argc < 2) { 17 | data = "%s%s%s%s"; 18 | //data = "%n%n%n%n"; 19 | } 20 | else { 21 | data = argv[1]; 22 | } 23 | 24 | printf(data); 25 | 26 | GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 27 | 28 | gtk_widget_show_all(GTK_WIDGET(window)); 29 | g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); 30 | 31 | gtk_main(); 32 | 33 | return 0; 34 | } 35 | -------------------------------------------------------------------------------- /test/e.c: -------------------------------------------------------------------------------- 1 | /* 2 | test crash app #5 3 | 4 | buffer overflow in network client 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define BUF_SIZE 64 16 | #define MAX_RECV 128 17 | #define HOST "127.0.0.1" 18 | #define PORT 8080 19 | 20 | int main() { 21 | char buffer[BUF_SIZE] = {0}; 22 | struct sockaddr_in serv_addr, client_addr; 23 | 24 | memset((void*)&serv_addr, 0, sizeof(serv_addr)); 25 | memset((void*)&client_addr, 0, sizeof(client_addr)); 26 | 27 | int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 28 | 29 | serv_addr.sin_family = AF_INET; 30 | serv_addr.sin_port = htons(PORT); 31 | 32 | inet_pton(AF_INET, HOST, &serv_addr.sin_addr); 33 | 34 | if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { 35 | printf("connection failed to %s:%d\n", HOST, PORT); 36 | return -1; 37 | } 38 | 39 | if(send(sock, buffer, 1, 0) < 0) { 40 | printf("send failed\n"); 41 | return -1; 42 | } 43 | 44 | if(recv(sock, buffer, MAX_RECV, 0) < 0) { 45 | printf("recv failed\n"); 46 | return -1; 47 | } 48 | 49 | printf("msg -> %s", buffer); 50 | 51 | close(sock); 52 | 53 | return 0; 54 | } 55 | -------------------------------------------------------------------------------- /test/f.c: -------------------------------------------------------------------------------- 1 | /* 2 | test crash app #6 3 | 4 | buffer overflow in network server 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define BUF_SIZE 64 16 | #define MAX_RECV 128 17 | #define PORT 8080 18 | 19 | int main() { 20 | char buffer[BUF_SIZE] = {0}; 21 | int on = 1; 22 | struct sockaddr_in serv_addr; 23 | 24 | memset((void*)&serv_addr, 0, sizeof(serv_addr)); 25 | 26 | int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 27 | 28 | if(sock < 0) { 29 | printf("socket failed\n"); 30 | return -1; 31 | } 32 | 33 | setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); 34 | 35 | serv_addr.sin_family = AF_INET; 36 | serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 37 | serv_addr.sin_port = htons(PORT); 38 | 39 | if(bind(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0) { 40 | printf("bind failed\n"); 41 | return -1; 42 | } 43 | 44 | if(listen(sock, 1) < 0) { 45 | printf("listen failed\n"); 46 | return -1; 47 | } 48 | 49 | int conn = accept(sock, (struct sockaddr*)NULL, NULL); 50 | 51 | if(conn < 0) { 52 | printf("accept failed\n"); 53 | return -1; 54 | } 55 | 56 | if(recv(conn, buffer, MAX_RECV, 0) < 0) { 57 | printf("recv failed\n"); 58 | return -1; 59 | } 60 | 61 | printf("msg -> %s", buffer); 62 | 63 | close(conn); 64 | close(sock); 65 | 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /test/linux/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for test crash apps (Linux) 3 | # 4 | 5 | CC = gcc 6 | CFLAGS = -w # -fsanitize=address 7 | GTKFLAGS = `pkg-config --cflags --libs gtk+-3.0` 8 | 9 | all: a b c d-gui e f 10 | 11 | a: ../a.c 12 | $(CC) $(CFLAGS) -o a ../a.c 13 | 14 | b: ../b.c 15 | $(CC) $(CFLAGS) -o b ../b.c 16 | 17 | c: ../c.c 18 | $(CC) $(CFLAGS) -o c ../c.c 19 | 20 | d-gui: ../d-gui.c 21 | $(CC) $(CFLAGS) -o d-gui ../d-gui.c $(GTKFLAGS) 22 | 23 | e: ../e.c 24 | $(CC) $(CFLAGS) -o e ../e.c 25 | 26 | f: ../f.c 27 | $(CC) $(CFLAGS) -o f ../f.c 28 | 29 | clean: 30 | rm a b c d-gui e f 31 | -------------------------------------------------------------------------------- /test/mac/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for test crash apps (Mac) 3 | # 4 | 5 | CC = clang 6 | CFLAGS = -w 7 | GTKFLAGS = `pkg-config --cflags --libs gtk+-3.0` 8 | 9 | all: a c d-gui e f 10 | 11 | a: ../a.c 12 | $(CC) $(CFLAGS) -o a ../a.c 13 | 14 | c: ../c.c 15 | $(CC) $(CFLAGS) -o c ../c.c 16 | 17 | d-gui: ../d-gui.c 18 | $(CC) $(CFLAGS) -o d-gui ../d-gui.c $(GTKFLAGS) 19 | 20 | e: ../e.c 21 | $(CC) $(CFLAGS) -o e ../e.c 22 | 23 | f: ../f.c 24 | $(CC) $(CFLAGS) -o f ../f.c 25 | 26 | clean: 27 | rm a c d-gui e f 28 | -------------------------------------------------------------------------------- /test/windows/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for test crash apps (Windows) 3 | # 4 | # notes 5 | # - no d-gui (gtk + pkg-config on win32 creates a ton of dependencies) 6 | # - no e/f socket examples (although fuzzing network services on win32 is supported) 7 | # - see event viewer -> windows logs -> application for Error logs (crashes) 8 | # 9 | 10 | CC = gcc 11 | CFLAGS = -w 12 | 13 | all: a b c 14 | 15 | a: ../a.c 16 | $(CC) $(CFLAGS) -o a ../a.c 17 | 18 | b: ../b.c 19 | $(CC) $(CFLAGS) -o b ../b.c 20 | 21 | c: ../c.c 22 | $(CC) $(CFLAGS) -o c ../c.c 23 | 24 | clean: 25 | del a.exe b.exe c.exe 26 | -------------------------------------------------------------------------------- /test_litefuzz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -Wignore 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # test_litefuzz.py 7 | # 8 | # py2> pytest 9 | # py3> python3 -m pytest 10 | # 11 | 12 | import os 13 | import sys 14 | import shlex 15 | 16 | import run 17 | import mutator 18 | import misc 19 | import config 20 | import settings 21 | from settings import SUCCESS, FAILURE 22 | 23 | ### functional tests (quick) ### 24 | 25 | # 26 | # run.main() 27 | # 28 | def test_run_main(): 29 | config.mode = settings.LOCAL 30 | 31 | if(misc.isWin32()): 32 | cmdline = 'C:\\Program Files (x86)\\GnuWin32\\bin\\true.exe' 33 | else: 34 | cmdline = 'true' 35 | 36 | cmdline = misc.setupCmdline(cmdline) 37 | 38 | config.maxtime = 2 39 | config.inputs = 'testing' 40 | config.iterations = 1 41 | 42 | if(not os.path.exists(config.inputs)): 43 | os.mkdir(config.inputs) 44 | 45 | with open(config.inputs + os.sep + 'test', 'w') as file: 46 | file.write('123') 47 | 48 | config.current_input = config.inputs + os.sep + 'test' 49 | 50 | misc.setupTmpRunDir() 51 | misc.setupNewIteration(cmdline) 52 | 53 | data = misc.readBytes(config.current_input) 54 | 55 | mutant = misc.getMutant(data) 56 | 57 | misc.writeBytes(settings.FUZZ_FILE, mutant) 58 | 59 | assert(run.main(cmdline) == SUCCESS) 60 | 61 | # 62 | # run.main() crash 63 | # 64 | def test_run_main_crash(): 65 | config.mode = settings.LOCAL 66 | 67 | if(misc.isWin32()): 68 | plat = 'windows' 69 | elif(misc.isMac()): 70 | plat = 'mac' 71 | else: 72 | plat = 'linux' 73 | 74 | # 75 | # target will crash regardless of fuzz file 76 | # 77 | if(misc.isWin32()): 78 | cmdline = 'test\\' + plat + '\\a.exe --read FUZZ' 79 | else: 80 | cmdline = 'test/' + plat + '/a --read FUZZ' 81 | 82 | cmdline = misc.setupCmdline(cmdline) 83 | 84 | config.maxtime = 2 85 | config.inputs = 'testing' 86 | config.iterations = 1 87 | 88 | if(not os.path.exists(settings.CRASH_DIR)): 89 | os.mkdir(settings.CRASH_DIR) 90 | 91 | if(not os.path.exists(config.inputs)): 92 | os.mkdir(config.inputs) 93 | 94 | with open(config.inputs + os.sep + 'test', 'w') as file: 95 | file.write('123') 96 | 97 | config.current_input = config.inputs + os.sep + 'test' 98 | 99 | misc.setupTmpRunDir() 100 | misc.setupNewIteration(cmdline) 101 | 102 | data = misc.readBytes(config.current_input) 103 | 104 | mutant = misc.getMutant(data) 105 | 106 | misc.writeBytes(settings.FUZZ_FILE, mutant) 107 | 108 | assert(run.main(cmdline) == SUCCESS) 109 | 110 | # 111 | # assumes an initially clean crash directory 112 | # 113 | assert((len(config.pc_list) > 0 and len(os.listdir(settings.CRASH_DIR)) > 0) or config.dups == 1) 114 | 115 | # net.main() 116 | # net.main() crash 117 | # core.repro() 118 | # core.minimize() 119 | 120 | ### unit tests ### 121 | 122 | # 123 | # mutator.flip() 124 | # 125 | def test_mutator_flip(): 126 | data = bytearray(b'test') 127 | size = len(data) 128 | 129 | mutant = mutator.flip(data, 1) 130 | 131 | assert(len(mutant) == size) 132 | 133 | # 134 | # mutator.highLow() 135 | # 136 | def test_mutator_highLow(): 137 | data = bytearray(b'test') 138 | size = len(data) 139 | 140 | mutant = mutator.highLow(data) 141 | 142 | assert(len(mutant) == size) 143 | 144 | # 145 | # mutator.insert() 146 | # 147 | def test_mutator_insert(): 148 | data = bytearray(b'test') 149 | size = len(data) 150 | 151 | mutant = mutator.insert(data) 152 | 153 | assert(len(mutant) > size) 154 | 155 | # 156 | # mutator.remove() 157 | # 158 | def test_mutator_remove(): 159 | data = bytearray(b'test') 160 | size = len(data) 161 | 162 | mutant = mutator.remove(data) 163 | 164 | assert(len(mutant) < size) 165 | 166 | # 167 | # mutator.carve() 168 | # 169 | def test_mutator_carve(): 170 | data = bytearray(b'test') 171 | size = len(data) 172 | 173 | mutant = mutator.carve(data) 174 | 175 | assert(len(mutant) < size) 176 | 177 | 178 | # 179 | # mutator.overwrite() 180 | # 181 | def test_mutator_overwrite(): 182 | data = bytearray(b'test') 183 | size = len(data) 184 | 185 | mutant = mutator.overwrite(data) 186 | 187 | assert(len(mutant) == size) 188 | 189 | 190 | # 191 | # mutator.radamsa() 192 | # 193 | def test_mutator_radamsa(): 194 | if(misc.isLinux() and (sys.version_info[0] >= 3)): 195 | data = bytearray(b'test') 196 | size = len(data) 197 | 198 | mutant = mutator.radamsa(data) 199 | 200 | assert(mutant != data) 201 | else: 202 | assert(1 == 1) 203 | 204 | 205 | # 206 | # misc.getWithin() 207 | # 208 | def test_misc_get_within(): 209 | i = 10 210 | data_len = 100 211 | size = 50 212 | 213 | assert(misc.getWithin(i, data_len, size) < data_len) 214 | 215 | # 216 | # misc.getRandomInt() 217 | # 218 | def test_get_random_int(): 219 | min = 0 220 | max = 100 221 | 222 | assert(misc.getRandomInt(min, max) <= max) 223 | 224 | # 225 | # misc.checkForExe() 226 | # 227 | def test_check_for_exe(): 228 | if(misc.isWin32()): 229 | exe_one = 'C:\\Windows\\system32\\calc.exe' 230 | exe_two = 'not_true.exe' 231 | else: 232 | exe_one = 'true' 233 | exe_two = 'not_true' 234 | 235 | assert(misc.checkForExe(exe_one)) 236 | assert(misc.checkForExe(exe_two) == False) 237 | -------------------------------------------------------------------------------- /triage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # litefuzz project 5 | # 6 | # triage.py 7 | # 8 | # 9 | 10 | import os 11 | import sys 12 | import glob 13 | import re 14 | import shutil 15 | 16 | import core 17 | import net 18 | import run 19 | import debug 20 | import misc 21 | import config 22 | import settings 23 | from settings import SUCCESS, FAILURE 24 | from settings import SIGTRAP, SIGABRT, SIGILL, SIGFPE, SIGSEGV, SIGGO 25 | 26 | def main(cmdline): 27 | if(misc.isUnix()): 28 | return unix(cmdline) 29 | elif(misc.isWin32()): 30 | return win32(cmdline) 31 | else: 32 | return FAILURE 33 | 34 | # 35 | # check debugger for crashes 36 | # 37 | def checkDebugger(cmdline): 38 | if(config.debug): 39 | print("entering checkDebugger()\n") 40 | 41 | # 42 | # both local server and insulated apps should use FUZZ_OUTPUTS_DEBUG 43 | # 44 | if(config.mode == settings.LOCAL_SERVER): # as non-crashing server sessions may be reused and reading from unique log files may fail 45 | try: 46 | with open(settings.FUZZ_OUTPUTS_DEBUG, 'rb') as file: # in case we get bytes 47 | log = file.read().decode('utf-8', 'ignore') 48 | except Exception as error: 49 | print("\n[ERROR] triage.checkDebugger() @ read(FUZZ_OUTPUTS_DEBUG): %s\n" % error) 50 | return FAILURE 51 | else: 52 | try: 53 | with open(settings.FUZZ_INFO, 'rb') as file: 54 | log = file.read().decode('utf-8', 'ignore') 55 | except Exception as error: 56 | print("\n[ERROR] triage.checkDebugger() @ read(FUZZ_INFO): %s\n" % error) 57 | return FAILURE 58 | 59 | # 60 | # if we catch an exception, run triage 61 | # 62 | for (ex, code) in settings.EXCEPTIONS.items(): 63 | if(ex in log): 64 | if(config.debug): 65 | print("[INFO] found exception %s in debugger\n" % ex) 66 | 67 | # 68 | # for insulated targets, just confirm the crash 69 | # 70 | if(config.insulate): 71 | misc.newCrash() 72 | 73 | else: 74 | config.returncode = code 75 | 76 | if(main(cmdline) != SUCCESS): 77 | print("[ERROR] triage.checkDebugger() calling triage.main() failed\n") 78 | return FAILURE 79 | 80 | # 81 | # if there is a crash, kill the process so we can cleanly restart it later 82 | # 83 | if(config.process != None): 84 | misc.killProcess(config.process.pid) 85 | misc.killProcessByName(config.target) 86 | 87 | return SUCCESS 88 | 89 | if(config.debug): 90 | print("[INFO] no crash found in debug log\n") 91 | 92 | return FAILURE # failed to find a crash 93 | 94 | # 95 | # save the repo and crash info on *nix OS 96 | # 97 | def unix(cmdline): 98 | if(config.debug): 99 | print("entering triage.unix()\n") 100 | 101 | info = "crash generated by mutating input '%s'\n\n" % os.path.basename(config.current_input) 102 | 103 | # 104 | # if we're in a debugger, don't use process.returncode 105 | # 106 | if(config.insulate or (config.mode == settings.LOCAL_SERVER)): 107 | returncode = config.returncode 108 | else: 109 | returncode = config.process.returncode 110 | 111 | # 112 | # if we attached it a process, use it for the cmdline / process name 113 | # 114 | if(config.attach): 115 | cmdline = [config.attach] 116 | 117 | if(returncode == SIGTRAP): 118 | fault = 'SIGTRAP' 119 | info += "cmdline %s exited with trace / breakpoint trap (%s)\n\n" % (cmdline, fault) 120 | elif(returncode == SIGABRT): 121 | fault = 'SIGABRT' 122 | info += "cmdline %s exited with abnormal termination condition (%s)\n\n" % (cmdline, fault) 123 | elif(returncode == SIGILL): 124 | fault = 'SIGILL' 125 | info += "cmdline %s exited with illegal instruction (%s)\n\n" % (cmdline, fault) 126 | elif(returncode == SIGFPE): 127 | fault = 'SIGFPE' 128 | info += "cmdline %s exited with floating point exception (%s)\n\n" % (cmdline, fault) 129 | elif(returncode == SIGSEGV): 130 | fault = 'SIGSEGV' 131 | info += "cmdline %s exited with invalid memory access (%s)\n\n" % (cmdline, fault) 132 | else: 133 | fault = 'UNKNOWN' 134 | info += "cmdline %s exited with %s\n\n" % (cmdline, fault) 135 | 136 | cmdl = '' 137 | 138 | for cmd in cmdline: 139 | cmdl += cmd + ' ' 140 | 141 | info += '-> ' 142 | 143 | if(settings.DEBUG_ENV_EFENCE_ENABLE): 144 | info += 'LD_PRELOAD=' + settings.LIBEFENCE_PATH + ' ' + cmdl + '\n\n' 145 | elif(settings.DEBUG_ENV_GLIBC_ENABLE): 146 | info += settings.GLIBC_MALLOC_CHECK + ' ' + cmdl + '\n\n' 147 | elif(settings.DEBUG_ENV_GMALLOC_ENABLE): 148 | info += 'DYLD_INSERT_LIBRARIES=' + settings.LIBGMALLOC_PATH + ' ' + cmdl + '\n\n' 149 | elif(settings.DEBUG_ENV_PAGEHEAP_ENABLE): 150 | info += 'enable pageheap + ' + cmdl + '\n\n' 151 | else: 152 | info += cmdl + '\n\n' 153 | 154 | if(config.debug): 155 | print("%s" % info) 156 | 157 | # 158 | # do a diff 159 | # 160 | if(settings.DIFF_ENABLE): 161 | if(misc.checkForExe("diff") and misc.checkForExe("od")): 162 | if(core.createDiff(config.current_input) != SUCCESS): 163 | print("[INFO] failed to create diff\n") 164 | 165 | # 166 | # auto-triage and create debug logs 167 | # 168 | # note: we already did triage beforehand for network apps to have FUZZ_INFO 169 | # 170 | if((config.mode == settings.LOCAL) or (config.mode == settings.LOCAL_CLIENT)): 171 | debug.main(cmdline) 172 | 173 | return saveCrash(fault, info) 174 | 175 | # 176 | # save the repo and crash info on Windows OS 177 | # 178 | # note: we've already got triage info from winappdbg, 179 | # so don't need to run debuggers like on linux and mac 180 | # 181 | def win32(fault_type, cmdline, crash_info): 182 | if(config.debug): 183 | print("entering triage.win32()\n") 184 | 185 | info = "crash generated by mutating input '%s'\n\n" % os.path.basename(config.current_input) 186 | 187 | if(config.debug): 188 | print("%s" % info) 189 | print("fault_type=%s, cmdline=%s\n" % (fault_type, cmdline)) 190 | 191 | if(fault_type == 0): 192 | fault = 'READ_AV' 193 | elif(fault_type == 1): 194 | fault = 'WRITE_AV' 195 | elif(fault_type == 8): 196 | fault = 'EXEC_AV' 197 | elif(str(fault_type) == 'EXCEPTION_INT_DIVIDE_BY_ZERO' or 198 | 'EXCEPTION_FLT_DIVIDE_BY_ZERO'): 199 | fault = 'DIV_BY_ZERO' 200 | else: 201 | fault = 'UNKNOWN_AV' 202 | 203 | if(cmdline != None): 204 | info += "%s encountered %s\n\n" % (cmdline, fault) 205 | else: 206 | info += "target encountered %s\n\n" % fault 207 | 208 | info += crash_info 209 | 210 | if(config.debug): 211 | print("%s" % info) 212 | 213 | # 214 | # do a diff 215 | # 216 | if(settings.DIFF_ENABLE): 217 | if(misc.checkForExe(settings.DIFF_WIN_BIN) and misc.checkForExe(settings.OD_WIN_BIN)): 218 | core.createDiff(config.current_input) 219 | else: 220 | print("\n[INFO] diff or od binaries not found, not doing a diff\n") 221 | 222 | return saveCrash(fault, info) 223 | 224 | # 225 | # save the crash info 226 | # 227 | def saveCrash(fault, info): 228 | if(config.debug): 229 | print("fault: %s" % fault) 230 | print("%s" % info) 231 | 232 | crash_info = None 233 | 234 | # 235 | # winappdbg (win32) has already given us this info 236 | # 237 | if(misc.isUnix()): 238 | # 239 | # both local server and insulated apps should use FUZZ_OUTPUTS_DEBUG 240 | # 241 | if(config.mode == settings.LOCAL_SERVER): # same as in checkDebugger() 242 | try: 243 | with open(settings.FUZZ_OUTPUTS_DEBUG, 'rb') as file: # in case we get bytes 244 | crash_info = file.read().decode('utf-8', 'ignore') 245 | except Exception as error: 246 | print("\n[ERROR] triage.saveCrash() @ read(FUZZ_OUTPUTS_DEBUG): %s\n" % error) 247 | return FAILURE 248 | else: 249 | try: 250 | with open(settings.FUZZ_INFO, 'rb') as file: 251 | crash_info = file.read().decode('utf-8', 'ignore') 252 | except Exception as error: 253 | print("\n[ERROR] triage.saveCrash() @ read(FUZZ_INFO): %s\n" % error) 254 | return FAILURE 255 | else: 256 | crash_info = info 257 | 258 | # 259 | # get pc 260 | # 261 | # Program received signal SIGSEGV, Segmentation fault. 262 | # 0x0000555555555136 in main () 263 | # 264 | pc = None 265 | 266 | if(misc.isLinux()): 267 | pc = re.search('0x(.*)\sin', crash_info) 268 | 269 | if(pc != None): 270 | pc = pc.group(1).lstrip('0') # py2 compat 271 | else: 272 | pc = "UNKNOWN" 273 | 274 | if(misc.isMac()): 275 | pc = re.search('->\s+0x(.*)<', crash_info) 276 | 277 | if(pc != None): 278 | pc = pc.group(1).split(' ')[0] 279 | else: 280 | pc = "UNKNOWN" 281 | 282 | if(misc.isWin32()): 283 | pc = re.search('ip=(.*)', crash_info) 284 | 285 | if(pc != None): 286 | pc = pc.group(1).split(' ')[0].lstrip('0') 287 | else: 288 | pc = "UNKNOWN" 289 | 290 | if(config.debug): 291 | print("pc=%s\n" % pc) 292 | 293 | config.current_pc = pc 294 | 295 | # 296 | # check for a duplicate crash (if we're not doing a repro) 297 | # 298 | if(config.repro == False): 299 | if(misc.checkDup(pc)): 300 | if(config.debug): 301 | print("\n[INFO] crash (pc=%s) is a dup\n" % pc) 302 | 303 | config.duplicate = True 304 | config.dups += 1 305 | 306 | if(config.min and (config.min_pc == None)): # only the first one (crash repro) 307 | config.min_pc = pc 308 | 309 | return SUCCESS 310 | 311 | # 312 | # if not a duplicate, welcome to the party 313 | # 314 | config.crash = True 315 | 316 | if(config.min): 317 | if(config.current_pc != config.min_pc): 318 | config.crashes += 1 319 | config.pc_list.append(pc) 320 | else: 321 | config.crashes += 1 322 | config.pc_list.append(pc) 323 | 324 | if(config.min and (config.min_pc == None)): 325 | config.min_pc = pc 326 | 327 | # 328 | # get bucket 329 | # 330 | bucket = None 331 | 332 | if(misc.isLinux()): 333 | bucket = re.search('Classification:\s(.*)', crash_info) 334 | 335 | if(bucket != None): 336 | #bucket = bucket[1] 337 | bucket = bucket.group(1) # py2 compat 338 | 339 | if(misc.isMac()): 340 | bucket = re.search('stop\sreason\s=\s(.*)', crash_info) 341 | 342 | if(bucket != None): 343 | bucket = bucket.group(1).split(' ')[0] 344 | 345 | # 346 | # already caught SIGABRT once, don't need to capture it here too 347 | # 348 | if(bucket == 'signal'): 349 | bucket = None 350 | 351 | if(misc.isWin32()): 352 | bucket = re.search('Security\srisk\slevel:\s(.*)', crash_info) 353 | 354 | if(bucket != None): 355 | bucket = bucket.group(1).upper().replace(' ', '_') 356 | 357 | if(config.debug): 358 | print("%s" % info) 359 | 360 | if(settings.ARTIFACTS_ENABLE): 361 | try: 362 | # mutant = open(settings.FUZZ_FILE, 'rb').read() 363 | mutant = misc.readBytes(settings.FUZZ_FILE) 364 | except Exception as error: 365 | print("\n[ERROR] triage.saveCrash() @ read(FUZZ_FILE): %s\n" % error) 366 | return FAILURE 367 | 368 | if(bucket != None): 369 | hash_file = settings.CRASH_DIR + \ 370 | os.sep + \ 371 | bucket + \ 372 | '_' + \ 373 | fault + \ 374 | '_' + \ 375 | pc + \ 376 | '_' + \ 377 | misc.getHash(mutant) 378 | else: 379 | hash_file = settings.CRASH_DIR + \ 380 | os.sep + \ 381 | fault + \ 382 | '_' + \ 383 | pc + \ 384 | '_' + \ 385 | misc.getHash(mutant) 386 | 387 | settings.CRASH_FILE = hash_file 388 | 389 | hash_out = hash_file + '.out' 390 | hash_info = hash_file + '.txt' 391 | hash_diff = hash_file + '.diff' 392 | hash_sdif = hash_file + '.diffs' 393 | 394 | hash_file = hash_file + '.' + config.file_ext 395 | 396 | misc.writeBytes(hash_file, mutant) 397 | 398 | try: 399 | with open(hash_info, 'w') as file: 400 | file.write(info) 401 | 402 | if(crash_info == None): 403 | print("\n[ERROR] triage.saveCrash() @ crash_info error\n") 404 | return FAILURE 405 | 406 | # 407 | # we already have crash info from winappdbg for win32 408 | # 409 | if(misc.isUnix()): 410 | file.write(crash_info) 411 | except Exception as error: 412 | print("\n[ERROR] triage.saveCrash() @ write(info): %s\n" % error) 413 | return FAILURE 414 | 415 | # 416 | # not an easy way to get stdout from the target in winappdbg yet 417 | # 418 | if(misc.isUnix()): 419 | if all([(config.insulate == False) and (config.mode != settings.LOCAL_SERVER) and (config.attach == None)]): 420 | try: 421 | shutil.copy(settings.FUZZ_OUTPUT, hash_out) 422 | except Exception as error: 423 | print("\n[ERROR] triage.saveCrash() @ copy FUZZ_OUTPUT: %s\n" % error) 424 | return FAILURE 425 | 426 | if(settings.DIFF_ENABLE): 427 | try: 428 | shutil.copy(settings.FUZZ_DIFF, hash_diff) 429 | except Exception as error: 430 | print("\n[ERROR] triage.saveCrash() @ copy FUZZ_DIFF: %s\n" % error) 431 | return FAILURE 432 | 433 | try: 434 | shutil.copy(settings.FUZZ_DIFF_STRING, hash_sdif) 435 | except Exception as error: 436 | print("\n[ERROR] triage.saveCrash() @ copy FUZZ_DIFF_STRING: %s\n" % error) 437 | return FAILURE 438 | 439 | if(config.debug): 440 | print("> %s" % hash_file) 441 | 442 | if(misc.isUnix()): 443 | print("> %s" % hash_out) 444 | 445 | print("> %s" % hash_info) 446 | 447 | if(settings.DIFF_ENABLE): 448 | print("> %s" % hash_diff) 449 | print("> %s" % hash_sdif) 450 | 451 | # 452 | # if we've triaged a crash with multiple input files, save a replayable repro (only the crashing packet for multibin) 453 | # 454 | if(config.multibin or config.multistr): 455 | saveRepro(settings.FUZZ_FILE) 456 | 457 | return SUCCESS 458 | 459 | # 460 | # check mac reportcrash logs 461 | # 462 | def reportCrash(): 463 | try: 464 | crash_files = glob.glob(settings.REPORT_CRASH_DIR + '/*') 465 | except Exception as error: 466 | print("\n[ERROR] triage.reportCrash() @ dir glob: %s\n" % error) 467 | return FAILURE 468 | 469 | for crash_file in crash_files: 470 | if(crash_file.endswith('.crash') and (os.path.basename(crash_file) not in config.report_list)): 471 | try: 472 | with open(crash_file, 'r') as file: 473 | crash_info = file.read() 474 | except Exception as error: 475 | print("\n[ERROR] triage.reportCrash() @ read(%s): %s\n" % (crash_file, error)) 476 | return FAILURE 477 | 478 | pc = re.search('0x(.*)', crash_info) 479 | 480 | if(pc != None): 481 | pc = pc.group(1).lstrip('0') # py2 compat 482 | else: 483 | pc = "UNKNOWN" 484 | 485 | if(misc.checkDup(pc)): 486 | if(config.debug): 487 | print("\n[INFO] crash (pc=%s) is a dup\n" % pc) 488 | 489 | config.duplicate = True 490 | config.dups += 1 491 | 492 | return False 493 | 494 | else: 495 | misc.newCrash() 496 | config.current_pc = pc 497 | config.pc_list.append(config.current_pc) 498 | 499 | config.report_list.append(os.path.basename(crash_file)) # update report list for no dups afterwards 500 | 501 | return True 502 | 503 | # 504 | # save repros for insulated targets, remote clients / servers, reportcrash'ers 505 | # 506 | def saveRepro(file): 507 | if(config.debug): 508 | print("entering saveRepro() with file=%s\n" % file) 509 | 510 | if(config.mode == settings.CLIENT): 511 | target = 'CLIENT' 512 | elif(config.mode == settings.SERVER): 513 | target = 'SERVER' 514 | else: 515 | target = 'UNKNOWN' 516 | 517 | if(config.insulate): 518 | mode = 'INSULATED' 519 | else: 520 | mode = 'REMOTE' 521 | 522 | if(config.report_crash): 523 | mode = 'REPORTCRASH' 524 | 525 | # 526 | # setup repro 527 | # 528 | if(file != None): 529 | repro = misc.readBytes(file) 530 | 531 | if(repro == None): 532 | print("[ERROR] triage.saveRepro() failed @ misc.readBytes()\n") 533 | return FAILURE 534 | else: 535 | repro = bytes(config.session[0]) # workaround 536 | 537 | # 538 | # save the previous test case too 539 | # 540 | if(file == settings.FUZZ_FILE): 541 | if(config.count > 1): # no previous fuzz file if it's the first iteration 542 | repro_prev = misc.readBytes(settings.FUZZ_FILE_PREV) 543 | else: 544 | repro_prev = None 545 | elif(file == None): 546 | repro_prev = config.session_prev 547 | else: 548 | repro_prev = None 549 | 550 | if(config.multibin or config.multistr): 551 | if(config.multibin): 552 | if(len(config.session_prev) == 0): 553 | if(config.debug): # this is OK if it eg. crashes on first iteration (there is no previous session) 554 | print("[ERROR] previous session data not found, cannot save this artifact\n") 555 | 556 | if(config.report_crash): 557 | hash_dir = settings.CRASH_DIR + \ 558 | os.sep + \ 559 | mode + \ 560 | '_' + \ 561 | target + \ 562 | '_' + \ 563 | config.host + \ 564 | '_' + \ 565 | str(config.port) + \ 566 | '_' + \ 567 | config.current_pc + \ 568 | '_' + \ 569 | misc.getHash(repro) 570 | elif(config.multibin): 571 | hash_dir = settings.CRASH_DIR + \ 572 | os.sep + \ 573 | mode + \ 574 | '_' + \ 575 | target + \ 576 | '_' + \ 577 | config.host + \ 578 | '_' + \ 579 | str(config.port) + \ 580 | '_' + \ 581 | misc.getHash(repro) 582 | else: # multistr 583 | hash_dir = settings.CRASH_DIR # no special dir for multistr, just write to crash dir 584 | 585 | # 586 | # could also add misc.getHash(repro_prev) 587 | # 588 | if(repro_prev != None): 589 | hash_dir_prev = settings.CRASH_DIR + \ 590 | os.sep + \ 591 | mode + \ 592 | '_' + \ 593 | target + \ 594 | '_' + \ 595 | config.host + \ 596 | '_' + \ 597 | str(config.port) + \ 598 | '_' + \ 599 | 'PREV' + \ 600 | '_' + \ 601 | misc.getHash(repro) 602 | 603 | if(config.multibin): 604 | try: 605 | # os.makedirs(hash_dir_prev) 606 | os.makedirs(hash_dir_prev + os.sep + 'repro') 607 | except Exception as error: 608 | if(config.debug): 609 | print("\n[INFO] mkdir failed for repro prev directories %s\n" % error) # may already exist 610 | 611 | if(config.multibin): 612 | try: 613 | os.makedirs(hash_dir + os.sep + 'repro') 614 | except Exception as error: 615 | if(config.debug): 616 | print("\n[INFO] mkdir failed for repro directories %s\n" % error) # may already exist 617 | 618 | for (i, s) in enumerate(config.session): 619 | hash_out = hash_dir + \ 620 | os.sep + \ 621 | 'repro' + \ 622 | os.sep + \ 623 | str(i + 1) + \ 624 | '.' + \ 625 | config.file_ext 626 | 627 | misc.writeBytes(hash_out, s) 628 | 629 | if(len(config.session_prev) != 0): 630 | for (i, s) in enumerate(config.session_prev): 631 | hash_out = hash_dir_prev + \ 632 | os.sep + \ 633 | 'repro' + \ 634 | os.sep + \ 635 | str(i + 1) + \ 636 | '.' + \ 637 | config.file_ext 638 | 639 | misc.writeBytes(hash_out, s) 640 | 641 | if(config.multistr): 642 | hash_out = hash_dir + \ 643 | os.sep + \ 644 | mode + \ 645 | '_' + \ 646 | target + \ 647 | '_' + \ 648 | config.host + \ 649 | '_' + \ 650 | str(config.port) + \ 651 | '_' + \ 652 | misc.getHash(repro) + \ 653 | '.' + \ 654 | config.file_ext 655 | 656 | if(repro_prev != None): 657 | hash_out_prev = hash_dir + \ 658 | os.sep + \ 659 | mode + \ 660 | '_' + \ 661 | target + \ 662 | '_' + \ 663 | config.host + \ 664 | '_' + \ 665 | str(config.port) + \ 666 | '_' + \ 667 | misc.getHash(repro) + \ 668 | '_' + \ 669 | 'PREV' + \ 670 | '.' + \ 671 | config.file_ext 672 | 673 | try: 674 | shutil.copy(settings.FUZZ_FILE_PREV, hash_out) 675 | except Exception as error: 676 | print("\n[ERROR] triage.saveRepro() @ copy repro prev (%s): %s\n" % (settings.FUZZ_FILE_PREV, error)) 677 | return FAILURE 678 | 679 | if(file != None): 680 | try: 681 | shutil.copy(file, hash_out) 682 | except Exception as error: 683 | print("\n[ERROR] triage.saveRepro() @ copy repro (%s): %s\n" % (file, error)) 684 | return FAILURE 685 | 686 | try: 687 | crash_files = glob.glob(settings.REPORT_CRASH_DIR + '/*') 688 | except Exception as error: 689 | print("\n[ERROR] triage.saveRepro() @ dir glob: %s\n" % error) 690 | return FAILURE 691 | 692 | crash_landing = settings.CRASH_DIR + os.sep + misc.getHash(repro) + '_' + os.path.basename(file) 693 | 694 | if(config.debug): 695 | print("copying ReportCrash file to %s\n" % crash_landing) 696 | 697 | for crash_file in crash_files: 698 | if(crash_file.endswith('.crash') and (os.path.basename(crash_file) not in config.report_list)): 699 | try: 700 | shutil.copy(crash_file, crash_landing) 701 | except Exception as error: 702 | print("\n[ERROR] triage.saveRepro() @ copy ReportCrash (%s): %s\n" % (crash_file, error)) 703 | return FAILURE 704 | 705 | # 706 | # copy over debugger logs 707 | # 708 | # note: for some reason, insulated debug logs may not show imcomplete crash info 709 | # 710 | if(config.attach or config.insulate): 711 | if(config.multibin): 712 | hash_info = hash_dir + \ 713 | os.sep + \ 714 | mode + \ 715 | '_' + \ 716 | target + \ 717 | '_' + \ 718 | config.host + \ 719 | '_' + \ 720 | str(config.port) + \ 721 | '.txt' 722 | else: 723 | hash_info = hash_out + '.txt' 724 | 725 | if(config.debug): 726 | print("copying debug logs %s -> %s\n" % (settings.FUZZ_INFO_STATIC, hash_info)) 727 | 728 | try: 729 | shutil.copy(settings.FUZZ_INFO_STATIC, hash_info) 730 | except Exception as error: 731 | print("\n[ERROR] triage.saveRepro() @ copy debug log (%s): %s\n" % (settings.FUZZ_INFO, error)) 732 | return FAILURE 733 | 734 | # 735 | # copy over standard remote server and client repros (single input, no sessions) 736 | # 737 | if(not(config.multibin or config.multistr)): 738 | if((config.mode == settings.CLIENT) or (config.mode == settings.SERVER)): 739 | hash_out = settings.CRASH_DIR + \ 740 | os.sep + \ 741 | mode + \ 742 | '_' + \ 743 | target + \ 744 | '_' + \ 745 | config.host + \ 746 | '_' + \ 747 | str(config.port) + \ 748 | '_' + \ 749 | misc.getHash(repro) + \ 750 | '.' + \ 751 | config.file_ext 752 | 753 | try: 754 | shutil.copy(file, hash_out) 755 | except Exception as error: 756 | print("\n[ERROR] triage.saveRepro() @ copy repro (%s): %s\n" % (file, error)) 757 | return FAILURE 758 | 759 | return SUCCESS 760 | --------------------------------------------------------------------------------