├── README.md ├── config_hodor.py ├── hodor.py ├── mutator_hodor.py ├── out_hodor.py ├── post_hodor.py ├── prep_hodor.py ├── qpqfiles ├── README.txt ├── qpqtest.txt └── qpqtest2.txt └── testfiles ├── bintest.png └── test.txt /README.md: -------------------------------------------------------------------------------- 1 | # Hodor Fuzzer : 2 | 3 | We want to design a general-use fuzzer that can be configured to use known-good 4 | input and delimiters in order to fuzz specific locations. Somewhere between a 5 | totally dumb fuzzer and something a little smarter, with significantly less effort 6 | than with implementation of a proper smart fuzzer. Hodor. 7 | 8 | We've had a few projects where a tool like this would have been useful. It's not 9 | uncommon to have some sort of case where we have known good input and want to 10 | modify it. If we know a bit about the file/protocol/etc spec, this could be 11 | used to easily do slightly smarter fuzzing. 12 | 13 | The design is intended to be portable so that one can just drop the files onto 14 | any box with python 2.7 and get cracking. 15 | 16 | Joel St. John, Braden Hollembaek, and Frank Arana 17 | 18 | 19 | **NOTE: POKE AROUND THE CONFIG FILE BEFORE RUNNING** 20 | 21 | Examples of usage: 22 | >`python hodor.py -t testfiles/test.txt` Mutate delimited text from test.txt 23 | `python hodor.py -t testfiles/test.txt -f ` Mutate all text from test.txt 24 | `python -c "print 'A' * 50" | python hodor.py -t -f` Pipe in input from another program, mutate all input 25 | `python hodor.py -b testfiles/bintest.png > diff.png` Mutate binary file at ranges specified in config_hodor.bin_fields (not compatible with qpq mode) 26 | Show where the files differ [address] [file1 hex] [file2 hex]: 27 | `cmp -l bintest.png diff.png | gawk '{printf "%08X %02X %02X\n", $1, strtonum(0$2), strtonum(0$3)}'` 28 | 29 | ### WISH LIST: 30 | * Make bflipper better 31 | * Add more mutators and ability to use multiple types simultaneously 32 | * Add features to qpq mode 33 | - Binary mode 34 | - Named-delimiters that allow for swap of same thing in all tokens of same name 35 | - Swap multiple tokens at once 36 | * Add more token handling features 37 | 38 | --- 39 | 40 | 41 | # Hodor Manual 42 | A general-use fuzzer that can be configured to use known-good input and delimiters in order to fuzz specific locations. 43 | 44 | Somewhere in between a dumb fuzzer and a proper smart fuzzer. Hodor. 45 | 46 | ## Table of Contents 47 | * Filetypes 48 | * Mutation Methods 49 | * Post Mutation Handler 50 | * Outputting Mutations 51 | * Performance 52 | 53 | 54 | ## Filetypes 55 | 56 | The first question is what kind of file are you mutating? Choosing a filetype is done at the command line. 57 | (i.e. `python hodor.py -b binfile` or `python hodor.py -t file.txt`) 58 | 59 | ### Full mutation: 60 | If you want to mutate the entire file sparing nothing, use the `-f` option. (i.e. `python hodor.py -t file.txt -f`) 61 | 62 | ### Mutating file segments: 63 | #### Binary Files 64 | Use `bin_fields` to choose which bytes to mutate in the binary. 65 | 66 | Example: 67 | If `bin_fields = [(0x3,0x7)]` 68 | First 16 bytes of the original file: 69 | `0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR` 70 | A few mutations: 71 | `0000000: 8950 4e55 0d0a 1a0a 0000 000d 4948 4452 .PNU........IHDR` 72 | `0000000: 8950 4e46 0d0a 1a0a 0000 000d 4948 4452 .PNF........IHDR` 73 | `0000000: 8950 4e47 0d0a 330a 0000 000d 4948 4452 .PNG..3.....IHDR` 74 | `0000000: 8950 4e47 0dd9 1a0a 0000 000d 4948 4452 .PNG........IHDR` 75 | 76 | **Note:** In Python's slicing, 0x3 does not get modified and is the byte before selection while 0x7 is the last byte to be mutated. A mathematical representation of this interval is (0x3, 0x7] 77 | 78 | #### For text files 79 | Use `text_delimiter` to decide which parts of your text will be mutated. 80 | 81 | Example: 82 | If `text_delimiter = "@@"` 83 | Original file: `In @@West Philadelphia@@ born and raised ` 84 | Mutated output: `In We_t Ih��a]Ih��al 85 | hi| born and raised` 86 | 87 | 88 | ## Mutation Methods 89 | 90 | Hodor has several mutation methods implemented for use. 91 | 92 | ### Millerfuzz 93 | The classic dumb fuzz algorithm by Charlie Miller. Millerfuzz mutation uses `FuzzFactor` to determine how minute the fuzzing will be. The higher the value in `FuzzFactor`, the more minute the mutation. 94 | 95 | ### Quid Pro Quo (QPQ) 96 | Quid Pro Quo is currently only partially implemented. 97 | 98 | Swap out tokens from the seed file with specific tokens of your choosing. If you specify more than one file, they will be aligned with each token from input in same order. If more than one file, you need to have same number of files as tokens for it to function correctly. 99 | 100 | For example, if you specify two files and have two tokens, the first token will be iteratively replaced by things from the first file, and the second token will be iteratively replaced with items from the second file. Only one change is made per output. 101 | 102 | Example: 103 | `qpq = {"file" : ["qpqfiles/qpqtest.txt", "qpqfiles/qpqtest2.txt"], 104 | "swapmode" : "oneatatime"}` 105 | 106 | ### TotesRand 107 | Replace the token with totally randomly output generated from Python's random.randrange() 108 | 109 | ### BFlipper 110 | Flip a determined amount of bits. Set `flipmode` to the number of bits to flip at once. If there are more iterations than bits, flipmode will increase automatically. BFlipper is deterministic. 111 | 112 | 113 | ## Post Mutation Handler 114 | 115 | ### CRC32 Fixup 116 | Hodor can compute and add a CRC32 checksum to the newly-mutated token. The CRC32 module has a few fields to configure: 117 | 118 | #### Type 119 | Can either be set for binary or text files. 120 | Options: `"type" : "bin"` or `"type" : "text"` 121 | 122 | #### Input_fields 123 | Works very similarly to `bin_fields` from the binary mutation section. All of the segments listed will be used to calculate the checksum. If `input_fields : None`, then the entire mutation will be used for computation and automatically append the sum to the end of the file. 124 | 125 | Example: 126 | `input_fields : [(0x12, 0x15), (0x4f, 0x5a)]` will compute the checksum for all segments *in the order listed*. 127 | 128 | **Note:** Remember, Python slices use the interval (x,y] 129 | 130 | #### Sum_location 131 | This designates where where to begin overwriting the location specified to insert the checksum. If set to `None` the sum will be appended to the end of the mutation. 132 | 133 | **Note:** `sum_location` cannot be a value that intercepts with any interval from `input_fields`, and `sum_location` will go unchecked if `input_fields` is set to `None` 134 | 135 | Example: 136 | `sum_location : [(0x0)]` will start overwriting the at the beginning of the file. 137 | 138 | 139 | ## Outputting Mutations 140 | What to do with the mutations. Logging is done differently per module. 141 | 142 | ### Stdout 143 | Print mutations directly to stdout. No other logging is done. 144 | 145 | ### Network 146 | Send mutation to network, indicate target and where to log results, 'file' will write unique files to specified directory. 147 | 148 | #### Connection 149 | 150 | The target host and port are set in `network`. 151 | 152 | Example: 153 | `network = {"remotehostport" : ("localhost", 1234)` 154 | 155 | Specify connection type by setting `ssl` to `True` or `False`. 156 | 157 | #### Logging 158 | 159 | Two options are available for logging. Using `file` allows to set a directory to write output to, and `stdout` will print to standard out. 160 | 161 | Example: 162 | 163 | `"log" : {"file" : "results/" }` 164 | 165 | ### Disk 166 | Write mutations as separate files into directory specified in `disk` 167 | 168 | Example: 169 | `disk = results/` will create the directory `results` in the current working directory, and store mutations within. 170 | 171 | ### Process 172 | Automatically send mutation to specified process. Hodor will log if the program crashes, or if the maximum wait time is reached. Either way the process is terminated and then the next iteration will begin. 173 | 174 | #### Name 175 | The name of the executable to run. 176 | 177 | Example: `"name" : "readelf"` 178 | 179 | #### Arguments 180 | Use `arguments` to add any arguments needed to run the executable listed in `name`. 181 | 182 | Example: `"arguments" : "-h"` will run `readelf -h`. 183 | 184 | If the mutation needs to be a command-line argument, set `"file_arg" : True`. If set to `False` the mutation will be send to the process as stdin. 185 | 186 | If the mutation needs a particular file extension use `extension` to designate it. 187 | 188 | Example: `"extension" : ".mp3"` 189 | 190 | Combining all the examples listed in this section, Hodor will execute `readelf -h mutation.mp3` 191 | 192 | #### Wait Time 193 | How long to wait before terminating an iteration of process if it does not crash. 194 | 195 | Example: 196 | Setting `"timeout" : 5` `"steps" : .3` Will check every .3 seconds to see if the process has crashed, and terminate at the final wait time of five seconds. 197 | 198 | #### Logging in process 199 | Handles how and what is stored through every iteration. These are located within the `log` portion of the process configuration. 200 | 201 | ##### What is always stored 202 | Some logs are automatically generated without configuration. A file `crashlog.txt` will be created containing every crash that occurs in the `path` directory specified. 203 | 204 | ##### How crashes are logged 205 | All files generated by logging will be located in `path`. 206 | Example: 207 | `"path" : "results/"` will create the directory `results` in the current working directory, and store log files within. 208 | 209 | ##### Logging Options 210 | Aside from `crashlog.txt`, setting `"mutations" : True` means every crash will have the offending mutation saved to a directory named after the terminating signal. 211 | 212 | Similarly, `"proc_out" : True` will record the stdout and stderr of *every* process to a text file. Including processes that do not crash. 213 | 214 | 215 | ## Performance 216 | ### Execution Delays 217 | Delays each mutation iteration. Useful for not flooding a network target. Values are measured in seconds. 218 | 219 | Example: 220 | `execdelay = .1` will delay every iteration by one-tenth of a second. 221 | 222 | ### Processes 223 | The amount of parallel iterations running at one time. It is recommended to only have one process per CPU core. Defined as `proc`. 224 | 225 | ### Threads 226 | Number of threads per-process to be used to handle the iterations. Useful for optimizing against I/O bound fuzzing. 227 | 228 | ### Iterations 229 | This is number of total iterations, and will be divided among procs and threads. Any number not divisible by procs*threads will just get the remainder dumped onto the last thread. 230 | 231 | 232 | --- 233 | 234 | That's it. Go find bugs! 235 | -------------------------------------------------------------------------------- /config_hodor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Config file for Hodor. Use Python data structures and vars to make things easy. 5 | """ 6 | 7 | # Number of seconds between executions per process, in case you don't want to flood a network target 8 | execdelay = 0 9 | 10 | # Used to specify tokens in text file that specify region to be fuzzed. 11 | # e.g. "West @@Philadelphia@@ born and raised." Will pass 'Philadelphia' to fuzz module 12 | # If you use a special regex character (. $ ^ { [ ( | ) ] } * + ? \), you may break things 13 | text_delimiter = "@@" 14 | 15 | # Binary mutation config options 16 | # Each tuple is a [begin,end] token for mutation from the input 17 | # e.g.[(0x3,0x18),(0x172,0x17D)] will mutate two areas of the binary, 0x3-0x18 and 0x172-0x17D 18 | bin_fields = [(0x3,0x18),(0x172,0x17D)] 19 | 20 | # Specify the number of processes to spawn to carry out the operation 21 | procs = 2 22 | 23 | threads = 4 24 | 25 | # Number of times to fuzz output, will be ignored for qpq (which just parses whole qpqfile). 26 | # This is number of total iterations, and will be divided among procs and threads, anything 27 | # not divisible by procs*threads will just get the remainder dumped onto the last thread 28 | iterations = 12 29 | 30 | # The classic dumb fuzz algorithm by Charlie Miller 31 | millerfuzz = {"FuzzFactor" : 100} # The higher the fuzz factor, the more minute the fuzzing 32 | 33 | # Quid pro quo, swap out tokens from the seed file with specific tokens of your choosing 34 | # If you specify more than one file, they will be aligned with each token from input in same order 35 | # If more than one file, you need to have same number of files as tokens for it to function correctly 36 | # See qpqfiles/README.txt for a description of pre-built files and available modes/types 37 | qpq = {"file" : ["qpqfiles/qpqtest.txt", "qpqfiles/qpqtest2.txt"], 38 | "swapmode" : "oneatatime"} 39 | 40 | # Replace the token with totally random output 41 | totesrand = {} 42 | 43 | # Select the flipmode, aka number of bits to flip at once 44 | # If there are more iterations than bits, flipmode will increase automatically 45 | bflipper = {"flipmode" : 1} 46 | 47 | # Uncomment the mutator you want to use, comment out all others 48 | # If you have multiple selected, things will probably screw up, so don't do that 49 | # If you comment them all out, no mutator is called 50 | mutator = { 51 | #"millerfuzz" : millerfuzz 52 | #"qpq" : qpq 53 | #"totesrand" : totesrand 54 | "bflipper" : bflipper 55 | } 56 | 57 | 58 | # Post handler selection and option specification 59 | # Do fixups before sending the output 60 | 61 | # CRC Fixup Module 62 | # "input_fields" for checksum 63 | # Each tuple is a [begin,end] token for what needs to be checksummed. 64 | # Starting number is exclusive, ending value is inclusive 65 | # If crc_input_fields = None, will checksum entire mutated data 66 | # "sum_location" = Where to write the checksum 67 | # WARNING: Cannot overlap above input fields. 68 | 69 | CRC32 = { 70 | "type": 71 | "bin", 72 | #"text", 73 | "input_fields": 74 | #[(0x12, 0x15), (0x4f, 0x5a)], 75 | None, 76 | "sum_location": 77 | [(0x0)] 78 | #None 79 | } 80 | 81 | post_handler = { 82 | #"add_CRC32" : CRC32 83 | } 84 | 85 | 86 | # Output handler selection and option specification 87 | # Available output handlers and their required configuration options 88 | 89 | # Dump output to standard out 90 | stdout = {} 91 | # Send to network, indicate target and where to log results, 'file' will write unique files to specified dir 92 | # Comment out both file and stdout for no logging. ssl must be set to True or False depending on need 93 | network = {"remotehostport" : ("localhost", 1234), 94 | "ssl" : False, 95 | "log" : {"path" : "results/" 96 | #"stdout" : True 97 | } 98 | } 99 | 100 | #Write output to directory 101 | disk = {"log" : {"path" : 'results/'}} 102 | 103 | # Uncomment in output_handler to call specified external process 104 | process = { 105 | "name" : "vlc", 106 | "arguments" : "", 107 | "file_arg" : True, # Use mutated filename as an argument 108 | "extension" : ".out", # extension to use for saved mutations 109 | "timeout" : 5, # seconds until subprocess is killed 110 | "steps" : .3, # steps in seconds 111 | "log" : { 112 | "path" : "results/", 113 | "mutations" : True, # save mutations that caused a crash? 114 | "proc_out" : False # save output from process that is being called? 115 | } 116 | } 117 | 118 | # Uncomment the one you want to actually use 119 | output_handler = { 120 | "stdout" : stdout 121 | #"disk" : disk 122 | #"network" : network 123 | #"process" : process 124 | } 125 | -------------------------------------------------------------------------------- /hodor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys, argparse, multiprocessing, threading, time, signal, os, math 4 | import config_hodor, prep_hodor, out_hodor 5 | 6 | def main(): 7 | # Parsing command line arguments 8 | helpmsg = "Welcome to the Hodor fuzzer!" 9 | parser = argparse.ArgumentParser(description=helpmsg) 10 | # Arguments listed in order of precedence, where applicable 11 | parser.add_argument("-s", "--stdin", 12 | help="Read fuzz seed from STDIN.", 13 | action="store_true") 14 | 15 | parser.add_argument('-t', '--textmode', 16 | nargs='?', 17 | const=sys.stdin, 18 | type=argparse.FileType('r'), 19 | help="Textmode, file required containing fuzz seed if not using stdin") 20 | 21 | parser.add_argument('-b', '--binmode', 22 | nargs='?', 23 | const=sys.stdin, 24 | type=argparse.FileType('rb'), 25 | help="Binarymode, file required containing fuzz seed if not using stdin") 26 | 27 | parser.add_argument("-f", "--fullmutate", 28 | help="Mutate entire input, ignore any delimiters", 29 | action="store_true") 30 | 31 | args = parser.parse_args() 32 | if not args.binmode and not args.textmode: 33 | print "Must select either binarymode or textmode" 34 | parser.print_help() 35 | sys.exit() 36 | build_logpaths() 37 | # Walk the args, call correct module 38 | indata = "" 39 | if args.stdin: 40 | while 1: 41 | try: 42 | line = sys.stdin.readline() 43 | except KeyboardInterrupt: 44 | break 45 | if not line: 46 | break 47 | indata += line 48 | handler = prep_hodor.parse_text if args.textmode else prep_hodor.parse_bin 49 | elif args.textmode and indata == "": 50 | indata = args.textmode.read() 51 | args.textmode.close() 52 | handler = prep_hodor.parse_text 53 | elif args.binmode and indata == "": 54 | if indata == "": 55 | indata = args.binmode.read() 56 | args.binmode.close() 57 | handler = prep_hodor.parse_bin 58 | plock = multiprocessing.Lock() 59 | global pjobs 60 | pjobs = [] 61 | signal.signal(signal.SIGINT, clean_kill) 62 | for i in xrange(config_hodor.procs): # We aren't going to use threads for qpq, just processes 63 | if 'qpq' in config_hodor.mutator: 64 | if args.binmode: 65 | print "qpq mode not compatible with binmode" # Need to add binmode compat someday.. 66 | sys.exit() 67 | p = multiprocessing.Process(name=str(i), target=prep_hodor.qpq_text, args=(indata, args.fullmutate, plock, False)) # Set tlock to false 68 | else: 69 | p = multiprocessing.Process(name=str(i), target=thread_helper, args=(indata, args.fullmutate, plock, handler)) 70 | pjobs.append(p) 71 | p.start() 72 | [p.join() for p in pjobs] 73 | return 74 | 75 | # Chop up threads for each process 76 | def thread_helper(indata, fullmutate, plock, handler): 77 | tlock = threading.Lock() 78 | tjobs = [] 79 | # Dispatch threads 80 | for i in xrange(config_hodor.threads): 81 | t = threading.Thread(name=str(i), target=exec_loop, args=(indata, fullmutate, plock, tlock, handler)) 82 | tjobs.append(t) 83 | t.start() 84 | [t.join() for t in tjobs] 85 | 86 | def exec_loop(indata, fullmutate, plock, tlock, handler): 87 | threadrem = 0 88 | if int(multiprocessing.current_process().name)+1 == config_hodor.procs and int(threading.current_thread().name)+1 == config_hodor.threads: 89 | threadrem = config_hodor.iterations % (config_hodor.procs * config_hodor.threads) 90 | iterations = (config_hodor.iterations/config_hodor.procs)/config_hodor.threads + threadrem 91 | threading.current_thread().name = str(int(threading.current_thread().name)*iterations) 92 | for i in xrange(iterations): 93 | if config_hodor.execdelay != 0: time.sleep(config_hodor.execdelay) 94 | handler(indata, fullmutate, plock, tlock) 95 | threading.current_thread().name = str(int(threading.current_thread().name)+1) 96 | 97 | def build_logpaths(): 98 | for handler in config_hodor.output_handler.itervalues(): 99 | if "log" in handler and "path" in handler["log"]: 100 | if not os.path.exists(handler["log"]["path"]): os.makedirs(handler["log"]["path"]) 101 | if handler == config_hodor.process: 102 | if not os.path.isfile(handler["log"]["path"] + "crashlog.txt"): 103 | out_hodor.loghelper(handler["log"]["path"] + "crashlog.txt", "Crash Log\n", "w") 104 | if config_hodor.process["log"]["proc_out"]: 105 | if not os.path.exists(handler["log"]["path"]+"proc_out/"): os.makedirs(handler["log"]["path"]+"proc_out/") 106 | 107 | 108 | def clean_kill(signal, frame): 109 | for p in pjobs: 110 | os.kill(p.pid, 9) 111 | sys.exit(0) 112 | 113 | 114 | if __name__ == '__main__': 115 | main() 116 | 117 | -------------------------------------------------------------------------------- /mutator_hodor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | This file contains all of the mutators for fuzzing. 5 | Pass in your data, watch it get mangled. 6 | """ 7 | import sys, math, random, multiprocessing, threading, inspect 8 | import config_hodor 9 | 10 | # Takes in a list of tokens to be mutated 11 | # Select mutator based on config_hodor.py 12 | def mutate(tokens): 13 | funclist = inspect.getmembers(sys.modules[__name__], inspect.isfunction) 14 | funcdict = {x[0]: x[1] for x in funclist} 15 | for mutator in config_hodor.mutator: 16 | tokens = funcdict[mutator](tokens) 17 | return tokens 18 | 19 | # Pass it a list of fields, will return a list of fuzzed fields 20 | # Based on the Charlie Miller 5 line fuzzer 21 | def millerfuzz(tokens): 22 | # The higher the fuzz factor, the more minute the fuzzing 23 | FuzzFactor = config_hodor.mutator['millerfuzz']['FuzzFactor'] 24 | mutated_tokens = [] 25 | for item in tokens: 26 | buf = bytearray(item) if isinstance(item, str) else item 27 | numwrites = random.randrange(math.ceil((float(len(buf)) / FuzzFactor))) + 1 28 | for j in range(numwrites): 29 | rbyte = random.randrange(256) 30 | rn = random.randrange(len(buf)) 31 | buf[rn] = "%c"%(rbyte) 32 | mutated_tokens.append(buf) 33 | return mutated_tokens 34 | 35 | # Just replace the tokens with totally random stuff 36 | def totesrand(tokens): 37 | mutated_tokens = [] 38 | for item in tokens: 39 | buf = bytearray(item) if isinstance(item, str) else item 40 | for j in range(len(buf)): 41 | rbyte = random.randrange(256) 42 | buf[j] = "%c"%(rbyte) 43 | mutated_tokens.append(buf) 44 | return mutated_tokens 45 | 46 | # Bitflipper, flips bits. Roughish implementation 47 | def bflipper(tokens): 48 | mutated_tokens = [] 49 | procnum = int(multiprocessing.current_process().name) 50 | threadnum = int(threading.current_thread().name) 51 | mystart = procnum*max((config_hodor.iterations/config_hodor.procs), 8) 52 | # Figure out how to spread threads in a sensible manner 53 | for item in tokens: 54 | buf = bytearray(item) if isinstance(item, str) else item 55 | if len(buf) == 0: 56 | mutated_tokens.append(buf) # Nothing to do 57 | continue 58 | # This is supposed to deal with iterations > buflen in a sane way 59 | # Should just loop through and flip more bits at once 60 | myflip = config_hodor.mutator["bflipper"]["flipmode"] + (mystart+threadnum)/(len(buf)*8) 61 | flipme = (threadnum/8)+(mystart/8) 62 | if flipme >= len(buf): 63 | flipme = flipme % len(buf) 64 | for j in range(myflip): 65 | buf[flipme] ^= (1 << ((threadnum+j)%8)) # Minor bug here, will do one extra xor on myflip>1 66 | mutated_tokens.append(buf) 67 | return mutated_tokens 68 | 69 | # Quid pro quo, swap out old tokens for user specified tokens 70 | def qpq(tokens): 71 | procnum = int(multiprocessing.current_process().name) 72 | mutated_tokens = [] 73 | for token in tokens: 74 | mutated_tokens.append([]) 75 | # Go through all files specified in config 76 | for filenum, files in enumerate(config_hodor.mutator['qpq']['file']): 77 | qpqfile = open(files, 'r') 78 | # Chop qpqfile into sections for each process to handle 79 | num_lines = sum(1 for line in qpqfile) 80 | qpqfile.seek(0) 81 | mylines = num_lines/config_hodor.procs 82 | mystart = procnum*mylines 83 | myend = (procnum+1)*mylines 84 | # Slow and inefficient way to get to next offset, must be a better way 85 | for i in range(mystart): 86 | qpqfile.readline() 87 | if procnum+1 != config_hodor.procs: 88 | for i in range(mystart, myend): 89 | newtok = qpqfile.readline().rstrip() 90 | if len(config_hodor.mutator['qpq']['file']) == 1: # if there is only one file, replace all tokens with same thing 91 | for idx, val in enumerate(mutated_tokens): 92 | mutated_tokens[idx].append(newtok) 93 | else: # else, each token is aligned with one file 94 | mutated_tokens[filenum].append(newtok) 95 | else: 96 | for line in qpqfile: 97 | newtok = line.rstrip() 98 | if len(config_hodor.mutator['qpq']['file']) == 1: # if there is only one file, replace all tokens with same thing 99 | for idx, val in enumerate(mutated_tokens): 100 | mutated_tokens[idx].append(newtok) 101 | else: # else, each token is aligned with one file 102 | mutated_tokens[filenum].append(newtok) 103 | qpqfile.close() 104 | return mutated_tokens 105 | 106 | -------------------------------------------------------------------------------- /out_hodor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | This module handles the exit of mutated output 5 | Send it across the network, print to STDOUT, dump into local bin, whatever. 6 | """ 7 | 8 | import sys, inspect, signal, threading, multiprocessing, os, ssl, socket 9 | import traceback, time, subprocess, select 10 | import config_hodor 11 | 12 | sub_pid = [] 13 | 14 | # Select the correct output handler 15 | def out(mutated_out, plock, tlock): 16 | funclist = inspect.getmembers(sys.modules[__name__], inspect.isfunction) 17 | funcdict = {x[0]: x[1] for x in funclist} 18 | for handler in config_hodor.output_handler: 19 | funcdict[handler](mutated_out, plock, tlock) 20 | 21 | def stdout(output, plock, tlock): 22 | plock.acquire() 23 | if tlock: tlock.acquire() 24 | sys.stdout.write(output) # Just dump to stdout for now 25 | if tlock: tlock.release() 26 | plock.release() 27 | 28 | # Send output over the network per config_hodor.network config options 29 | def network(mutated_out, plock, tlock): 30 | try: 31 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | if config_hodor.network["ssl"]: s = ssl.wrap_socket(s, cert_reqs=ssl.CERT_NONE) # Not checking certs for fuzzing.. 33 | s.connect(config_hodor.network["remotehostport"]) 34 | s.send(mutated_out) 35 | s.setblocking(0) 36 | retdata = "" 37 | ready = select.select([s], [], [], 10) # Ten second max timeout, may want to alter if target is limping 38 | if ready[0]: 39 | retdata = s.recv(4096) 40 | except socket.error as (errno, sockerr): 41 | sys.stderr.write("SOCKET ERROR({0}): {1}\n".format(errno, sockerr)) 42 | s.close() 43 | return 44 | s.close() 45 | # Handle logging 46 | if "path" in config_hodor.network["log"]: 47 | logfile = config_hodor.network["log"]["path"] 48 | uid = "%s-%s-%s" % (multiprocessing.current_process().pid, threading.current_thread().name, time.clock()) 49 | infile = "%s%s.sent" % (logfile,uid) 50 | outfile = "%s%s.recv" % (logfile,uid) 51 | loghelper(infile, mutated_out, 'wb') 52 | loghelper(outfile, retdata, 'wb') 53 | if "stdout" in config_hodor.network["log"]: 54 | dump = "\nSENT:\n%s\nRECEIVED:\n%s" % (mutated_out,retdata) 55 | stdout(dump, plock, tlock) 56 | # else we log nothing 57 | 58 | def disk(output, plock, tlock): 59 | uid = "%s-%s-%s" % (multiprocessing.current_process().pid, threading.current_thread().name, time.clock()) 60 | filename = "%s/%s" % (config_hodor.disk["log"]["path"], uid) 61 | loghelper(filename, output, 'wb') 62 | 63 | def process(mutated_out, plock, tlock): 64 | if "name" not in config_hodor.process: 65 | print "Missing process name (Process is enabled.)" 66 | exit(9001) 67 | name = config_hodor.process["name"] 68 | args = "" if "arguments" not in config_hodor.process else config_hodor.process["arguments"] 69 | try: 70 | logpath = config_hodor.process["log"]["path"] 71 | if tlock: tlock.acquire() 72 | mutationsig = "%s-%s-%s" % (multiprocessing.current_process().pid, threading.current_thread().name, time.clock()) 73 | if config_hodor.process["log"]["proc_out"]: 74 | proc_out = open(logpath+"proc_out/"+mutationsig+".proc_out", 'w') 75 | file_out = proc_out 76 | else: file_out = subprocess.PIPE 77 | if "file_arg" in config_hodor.process: 78 | tempfilename = "tempfile%s%s" % (mutationsig,config_hodor.process["extension"]) 79 | fullpath = "%s/%s" % (logpath, tempfilename) 80 | loghelper(fullpath, mutated_out, "w") 81 | execution = "exec %s %s %s/%s" % (name,args,logpath,tempfilename) 82 | p = subprocess.Popen(execution, shell=True, stdout=file_out, stdin=subprocess.PIPE, stderr=file_out) 83 | pout = str(p.returncode) 84 | else: 85 | execution = "%s %s" % (name, args) 86 | p = subprocess.Popen(execution, shell=True, stdout=file_out, stdin=subprocess.PIPE, stderr=file_out) 87 | p.stdin.write(mutated_out) 88 | p.stdin.close() 89 | pout = str(p.returncode) 90 | sub_pid.append(p.pid) 91 | timeout = config_hodor.process["timeout"] 92 | steps = config_hodor.process["steps"] 93 | timer = 0 94 | exit_status = None 95 | while(timer < timeout and exit_status==None): 96 | time.sleep(steps) 97 | exit_status = p.poll() 98 | timer += steps 99 | if exit_status: # Not the best way to do this, doing your own instrumentation might be better 100 | # Crashed process 101 | signame = signal_name(128 - (exit_status % 128)) # Crude method to get signal code 102 | entry = "%s Exit status: %s\n" % (mutationsig,signame) 103 | crash_filename = "%scrashlog.txt" % (logpath) 104 | loghelper(crash_filename, entry, 'a') 105 | if config_hodor.process["log"]["mutations"]: 106 | signal_path = "%s%s" % (logpath,signame) 107 | try: 108 | if not os.path.exists(signal_path): os.makedirs(signal_path) 109 | except e: 110 | pass 111 | mutated_filename = "%s/%s%s" % (signal_path, mutationsig, config_hodor.process["extension"]) 112 | loghelper(mutated_filename, mutated_out, "w") 113 | elif exit_status == None: 114 | try: # Kill running process 115 | p.kill() 116 | p.communicate() 117 | except OSError, errno: 118 | pass 119 | # Delete this file 120 | if not os.path.isfile(fullpath): 121 | exit(9100) # Something happened to our tempfile? 122 | else: 123 | os.remove(fullpath) 124 | if tlock: tlock.release() 125 | sub_pid.remove(p.pid) 126 | except Exception, e: 127 | print "Exception caught:\n" 128 | pout = traceback.print_exc() 129 | if tlock: tlock.release() 130 | exit(9001) 131 | finally: 132 | if config_hodor.process["log"]["proc_out"]: proc_out.close() 133 | 134 | def loghelper(filename, data, mode): 135 | f = open(filename, mode) 136 | f.write(data) 137 | f.close() 138 | 139 | def signal_name(num): 140 | signames = [] 141 | for key in signal.__dict__.keys(): 142 | if key.startswith("SIG") and getattr(signal, key) == num: 143 | signames.append (key) 144 | if len(signames) == 1: 145 | return signames[0] 146 | else: 147 | return str(num) 148 | -------------------------------------------------------------------------------- /post_hodor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | This module handles the processing of mutated output 5 | Do final modifications to data then send to an output mode 6 | """ 7 | 8 | import sys, zlib, inspect 9 | import config_hodor, out_hodor 10 | 11 | # Select the correct post-fuzz handler, then fire to output 12 | def handler(mutated_out, plock, tlock): 13 | funclist = inspect.getmembers(sys.modules[__name__], inspect.isfunction) 14 | funcdict = {x[0]: x[1] for x in funclist} 15 | for handler in config_hodor.post_handler: 16 | funcdict[handler](mutated_out, plock, tlock) 17 | out_hodor.out(mutated_out, plock, tlock) 18 | 19 | # Generates checksum and allows specification of offset 20 | # if no offset given, will default to append checksum at the end 21 | def add_CRC32(mutated_out, plock, tlock): 22 | # Check config entries are valid: 23 | if config_hodor.CRC32["input_fields"] and config_hodor.CRC32["sum_location"]: 24 | for fields in config_hodor.CRC32["input_fields"]: 25 | if (config_hodor.CRC32["input_fields"][0] > fields[0] and config_hodor.CRC32['sum_location'][0] <= fields[1]): 26 | print "Invalid CRC32['sum_location']: " + hex(config_hodor.CRC32['sum_location'][0]) + " Cannot overwrite CRC32['input_fields']: " + hex(fields[0]) + " " + hex(fields[1]) 27 | exit(1) 28 | # Creating checksum 29 | # if input specified, create list of pieces 30 | if config_hodor.CRC32['input_fields']: 31 | sum_pieces = [] 32 | for fields in config_hodor.CRC32['input_fields']: 33 | sum_pieces.append(mutated_out[fields[0]:fields[1]]) 34 | # Only one piece: 35 | if len(config_hodor.CRC32['input_fields']) == 1: 36 | checksum = zlib.crc32(str(sum_pieces[0])) & 0xffffffff 37 | # Multiple pieces: 38 | elif len(config_hodor.CRC32['input_fields']) > 1: 39 | for i in sum_pieces: 40 | checksum = zlib.crc32(str(i),0) if i == sum_pieces[0] else (zlib.crc32(str(i), checksum) & 0xffffffff) 41 | else: # Sum all data 42 | checksum = zlib.crc32(str(mutated_out)) & 0xffffffff 43 | checksum = '%x' % checksum 44 | checksum = checksum.zfill(8) #Fixes leading 0 problem 45 | checksum = bytearray(checksum) 46 | # Determine where to put checksum 47 | if "bin" in config_hodor.CRC32['type']: 48 | if config_hodor.CRC32['sum_location']: 49 | mutated_out[config_hodor.CRC32['sum_location'][0]:(config_hodor.CRC32['sum_location'][0] + 8)] = checksum.decode("hex") 50 | else: 51 | mutated_out += checksum.decode("hex") 52 | elif "text" in config_hodor.CRC32['type']: 53 | if config_hodor.CRC32['sum_location']: 54 | mutated_out[config_hodor.CRC32['sum_location']:(config_hodor.CRC32['sum_location'] + 8)] = checksum 55 | else: 56 | mutated_out += checksum 57 | else: 58 | print "CRC32['type'] not specified in config_hodor.py" 59 | exit(1) 60 | return mutated_out 61 | 62 | -------------------------------------------------------------------------------- /prep_hodor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | This is the Hodor module that will prep the files for fuzzing. 5 | Handles delimited text files as well as binary files with formatting info. 6 | Delimiters can be specified by config file and are $$ by default. 7 | """ 8 | import re, string, time 9 | import config_hodor, mutator_hodor, post_hodor 10 | 11 | # Takes in text blob, pulls strings delimited by text_delimeter (or not), sends to mutator 12 | # Sends bytearray of mutated output to post_hodor.handler() for further processing 13 | # tlock is set to false by things that aren't utilizing threading. plock is always used 14 | def parse_text(filetext, ignore_tokens, plock, tlock): 15 | if ignore_tokens: 16 | filetext = [filetext] # mutate expects a list 17 | mutated_text = mutator_hodor.mutate(filetext)[0] 18 | else: 19 | delim = config_hodor.text_delimiter 20 | regexp = "%s([\s\S]*?)%s" % (delim, delim) 21 | tokens = re.findall(regexp, filetext) 22 | toklocs = [m.start() for m in re.finditer(regexp, filetext)] 23 | mutated_tokens = mutator_hodor.mutate(tokens) 24 | # Replace original input with mutated output 25 | for idx, val in enumerate(tokens): 26 | filetext = filetext[:2+toklocs[idx]] + mutated_tokens[idx] + filetext[toklocs[idx]+2+len(mutated_tokens[idx]):] 27 | mutated_text = bytearray(string.replace(filetext, delim, "")) 28 | post_hodor.handler(mutated_text, plock, tlock) 29 | return 30 | 31 | # Takes in a binary blob, pulls fields specified in bin_fields (or not), sends to mutator 32 | # Sends bytearray of mutated output to post_hodor.handler() for further processing 33 | def parse_bin(filebytes, ignore_fields, plock, tlock): 34 | if ignore_fields: 35 | filebytes = [filebytes] 36 | mutated_bytes = mutator_hodor.mutate(filebytes)[0] 37 | else: 38 | tokens = [] 39 | for fields in config_hodor.bin_fields: 40 | tokens.append(filebytes[fields[0]:fields[1]]) 41 | mutated_tokens = mutator_hodor.mutate(tokens) 42 | mutated_bytes = bytearray(filebytes) 43 | for idx, val in enumerate(config_hodor.bin_fields): 44 | mutated_bytes[val[0]:val[1]] = mutated_tokens[idx] 45 | mutated_bytes = bytearray(mutated_bytes) 46 | post_hodor.handler(mutated_bytes, plock, tlock) 47 | return 48 | 49 | # qpq mode requires some different stuff 50 | def qpq_text(filetext, ignore_tokens, plock, tlock): 51 | if ignore_tokens: 52 | filetext = [filetext] # mutate expects a list 53 | mutated_text = mutator_hodor.qpq(filetext) 54 | mutated_text = bytearray(mutated_text[0][0]) 55 | post_hodor.handler(mutated_text, plock, tlock) 56 | else: 57 | delim = config_hodor.text_delimiter 58 | regexp = delim + "([\s\S]*?)" + delim 59 | tokens = re.findall(regexp, filetext) 60 | toklocs = [m.start() for m in re.finditer(regexp, filetext)] 61 | mutated_tokens = mutator_hodor.qpq(tokens) 62 | # Replace original input with mutated output 63 | # qpq returns a list of lists of new tokens for each delimmed token 64 | for idx, val in enumerate(tokens): 65 | for newtok in mutated_tokens[idx]: 66 | mutated_text = filetext[:2+toklocs[idx]] + newtok + filetext[toklocs[idx]+2+len(val):] 67 | mutated_text = bytearray(string.replace(mutated_text, delim, "")) 68 | if config_hodor.execdelay != 0: time.sleep(config_hodor.execdelay) 69 | post_hodor.handler(mutated_text, plock, tlock) 70 | return 71 | 72 | -------------------------------------------------------------------------------- /qpqfiles/README.txt: -------------------------------------------------------------------------------- 1 | Quid Pro Quo is partially implemented but still functional for many purposes. 2 | 3 | 4 | If you specify one qpqfile in config_hodor, tokens from that file will be swapped sequentially with each 5 | delimited token, one change at a time. If you specify multiple qpqfiles, you MUST specify the same number 6 | of qpqfiles as delimited tokens in the input file, or behavior is undefined. 7 | 8 | For example, if you specify two files and have two tokens, the first token will be iteratively replaced 9 | by things from the first file, and the second token will be iteratively replaced with items from the 10 | second file. Only one change is made per output. 11 | 12 | qpqtest.txt and qpqtest2.txt are brief PoC files for use with test.txt 13 | -------------------------------------------------------------------------------- /qpqfiles/qpqtest.txt: -------------------------------------------------------------------------------- 1 | South Detroit 2 | East Baltimore 3 | -------------------------------------------------------------------------------- /qpqfiles/qpqtest2.txt: -------------------------------------------------------------------------------- 1 | Malibu 2 | Beverly Hills 3 | Santa Monica 4 | -------------------------------------------------------------------------------- /testfiles/bintest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nccgroup/Hodor/01be1077a1ede236fac78103816e7d58b64e43e6/testfiles/bintest.png -------------------------------------------------------------------------------- /testfiles/test.txt: -------------------------------------------------------------------------------- 1 | In @@West Philadelphia@@ born and raised 2 | On the playground was where I spent most of my days 3 | Chillin' out maxin' relaxin' all cool 4 | And all shootin some b-ball outside of the school 5 | When a couple of guys who were up to no good 6 | Started making trouble in my neighborhood 7 | I got in one little fight and my mom got scared 8 | She said 'You're movin' with your auntie and uncle in @@Bel Air@@' 9 | --------------------------------------------------------------------------------