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