1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
|
#!/usr/bin/python
# Copyright 2008 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
This module implements a fuzzer for sel_ldr's ELF parsing / NaCl
module loading functions.
The fuzzer takes as arguments a pre-built nexe and sel_ldr, and will
randomly modify a copy of the nexe and run sel_ldr with the -F flag.
If/when sel_ldr crashes, the copy of the nexe is saved.
"""
from __future__ import with_statement # pre-2.6
import getopt
import os
import random
import re
import signal
import subprocess
import sys
import tempfile
import elf
max_bytes_to_fuzz = 16
default_progress_period = 64
def uniform_fuzz(input_string, nbytes_max):
nbytes = random.randint(1, nbytes_max) # fuzz at least one byte
# pick n distinct values from [0... len(input_string)) uniformly and
# without replacement.
targets = random.sample(xrange(len(input_string)), nbytes)
targets.sort()
# each entry of keepsies is a tuple (a-1,b) of indices indicating
# the non-fuzzed substrings of input_string.
keepsies = zip([-1] + targets,
targets + [len(input_string)])
# the map is essentially a generator of keepsie substrings followed
# by a random byte. joined together -- and throwing away the extra,
# trailing random byte -- is the fuzzed string.
return ''.join(input_string[subrange[0] + 1 : subrange[1]] +
chr(random.randint(0, 255))
for subrange in keepsies)[:-1]
#enddef
def simple_fuzz(nexe_elf):
orig = nexe_elf.elf_str
start_offset = nexe_elf.ehdr.phoff
length = nexe_elf.ehdr.phentsize * nexe_elf.ehdr.phnum
end_offset = start_offset + length
return (orig[:start_offset] +
uniform_fuzz(orig[start_offset
:end_offset],
max_bytes_to_fuzz) +
orig[end_offset:])
#enddef
def genius_fuzz(nexe_elf):
print >>sys.stderr, 'Genius fuzzer not implemented yet.'
# parse as phdr and use a distribution that concentrates on certain fields
sys.exit(1 + hash(nexe_elf)) # ARGSUSED
#enddef
available_fuzzers = {
'simple' : simple_fuzz,
'genius' : genius_fuzz,
}
def usage(stream):
print >>stream, """\
Usage: elf_fuzzer.py [-d destination_dir]
[-D destination_for_log_fatal]
[-f fuzzer]
[-i iterations]
[-m max_bytes_to_fuzz]
[-n nexe_original]
[-p progress_output_period]
[-s sel_ldr]
[-S seed_string_for_rng]
-d: Directory in which fuzzed files that caused core dumps are saved.
Default: "."
-D: Directory for saving crashes from LOG_FATAL errors. Default: discarded.
-f: Fuzzer to use. Available fuzzers are:
%s
-i: Number of iteration to fuzz. Default: -1 (infinite).
For use as a large test, set to a finite value.
-m: Maximum number of bytes to change. A random choice of one to this
number of bytes in the fuzz template's program header will be replaced
with a random value.
-n: Nexes to fuzz. Multiple nexes may be specified by using -n repeatedly,
in which case each will be used in turn as the fuzz template.
-p: Progress indicator period. Print a character for every this many fuzzing
runs. Requires verbosity to be at least 1. Default is %d.
-S: Seed_string_for_rng is used to seed the random module's random number
generator; any string will do -- it is hashed.
""" % (', '.join(available_fuzzers.keys()), default_progress_period)
#enddef
def choose_progress_char(num_saved):
return '0123456789abcdef'[num_saved % 16]
def main(argv):
global max_bytes_to_fuzz
sel_ldr_path = None
nexe_path = []
dest_dir = '.'
dest_fatal_dir = None # default: do not save
iterations = -1
fuzzer = 'simple'
verbosity = 0
progress_period = default_progress_period
progress_char = '.'
num_saved = 0
try:
opt_list, args = getopt.getopt(argv[1:], 'd:D:f:i:m:n:p:s:S:v')
except getopt.error, e:
print >>sys.stderr, e
usage(sys.stderr)
return 1
#endtry
for (opt, val) in opt_list:
if opt == '-d':
dest_dir = val
elif opt == '-D':
dest_fatal_dir = val
elif opt == '-f':
if available_fuzzers.has_key(val):
fuzzer = val
else:
print >>sys.stderr, 'No fuzzer:', val
usage(sys.stderr)
return 1
#endif
elif opt == '-i':
iterations = long(val)
elif opt == '-m':
max_bytes_to_fuzz = int(val)
elif opt == '-n':
nexe_path.append(val)
elif opt == '-p':
progress_period = int(val)
elif opt == '-s':
sel_ldr_path = val
elif opt == '-S':
random.seed(val)
elif opt == '-v':
verbosity = verbosity + 1
else:
print >>sys.stderr, 'Option', opt, 'not understood.'
return -1
#endif
#endfor
if progress_period <= 0:
print >>sys.stderr, 'verbose progress indication period must be positive.'
return 1
#endif
if not nexe_path:
print >>sys.stderr, 'No nexe specified.'
return 2
#endif
if sel_ldr_path is None:
print >>sys.stderr, 'No sel_ldr specified.'
return 3
#endif
if verbosity > 0:
print 'sel_ldr is at', sel_ldr_path
print 'nexe prototype(s) are at', nexe_path
#endif
nfa = re.compile(r'LOG_FATAL abort exit$')
which_nexe = 0
while iterations != 0:
nexe_bytes = open(nexe_path[which_nexe % len(nexe_path)]).read()
nexe_elf = elf.Elf(nexe_bytes)
fd, path = tempfile.mkstemp()
try:
fstream = os.fdopen(fd, 'w')
fuzzed_bytes = available_fuzzers[fuzzer](nexe_elf)
fstream.write(fuzzed_bytes)
fstream.close()
cmd_arg_list = [ sel_ldr_path,
'-F',
'--', path]
p = subprocess.Popen(cmd_arg_list,
stdin = subprocess.PIPE, # no /dev/null on windows
stdout = subprocess.PIPE,
stderr = subprocess.PIPE)
(out_data, err_data) = p.communicate(None)
if p.returncode < 0:
if verbosity > 1:
print 'sel_ldr exited with status', p.returncode, ', output.'
print 79 * '-'
print 'standard output'
print 79 * '-'
print out_data
print 79 * '-'
print 'standard error'
print 79 * '-'
print err_data
elif verbosity > 0:
os.write(1, '*')
#endif
if (os.WTERMSIG(-p.returncode) != signal.SIGABRT or
nfa.search(err_data) == None):
with os.fdopen(tempfile.mkstemp(dir=dest_dir)[0], 'w') as f:
f.write(fuzzed_bytes)
#endwith
# this is a one-liner alternative, relying on the dtor of
# file-like object to handle the flush/close. assumption
# here as with the 'with' statement version: write errors
# would cause an exception.
#
# os.fdopen(tempfile.mkstemp(dir=dest_dir)[0],
# 'w').write(fuzzed_bytes)
num_saved = num_saved + 1
progress_char = choose_progress_char(num_saved)
else:
if dest_fatal_dir is not None:
with os.fdopen(tempfile.mkstemp(dir=dest_fatal_dir)[0], 'w') as f:
f.write(fuzzed_bytes)
#endwith
num_saved = num_saved + 1
progress_char = choose_progress_char(num_saved)
elif verbosity > 1:
print 'LOG_FATAL exit, not saving'
#endif
#endif
#endif
finally:
os.unlink(path)
#endtry
if iterations > 0:
iterations = iterations - 1
#endif
if verbosity > 0 and which_nexe % progress_period == 0:
os.write(1, progress_char)
#endif
which_nexe = which_nexe + 1
#endwhile
print 'A total of', num_saved, 'nexes caused sel_ldr to exit with a signal.'
#enddef
if __name__ == '__main__':
sys.exit(main(sys.argv))
#endif
|