File: elf_fuzzer.py

package info (click to toggle)
chromium-browser 41.0.2272.118-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie-kfreebsd
  • size: 2,189,132 kB
  • sloc: cpp: 9,691,462; ansic: 3,341,451; python: 712,689; asm: 518,779; xml: 208,926; java: 169,820; sh: 119,353; perl: 68,907; makefile: 28,311; yacc: 13,305; objc: 11,385; tcl: 3,186; cs: 2,225; sql: 2,217; lex: 2,215; lisp: 1,349; pascal: 1,256; awk: 407; ruby: 155; sed: 53; php: 14; exp: 11
file content (278 lines) | stat: -rwxr-xr-x 8,089 bytes parent folder | download
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