| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import base64 |
| 6 import functools |
| 7 import itertools |
| 8 import os |
| 9 import random |
| 10 import re |
| 11 import string |
| 12 import sys |
| 13 import textwrap |
| 14 |
| 15 from . import utils |
| 16 |
| 17 |
| 18 def FuzzyInt(n): |
| 19 """Returns an integer derived from the input by one of several mutations.""" |
| 20 int_sizes = [8, 16, 32, 64, 128] |
| 21 mutations = [ |
| 22 lambda n: utils.UniformExpoInteger(0, sys.maxint.bit_length() + 1), |
| 23 lambda n: -utils.UniformExpoInteger(0, sys.maxint.bit_length()), |
| 24 lambda n: 2 ** random.choice(int_sizes) - 1, |
| 25 lambda n: 2 ** random.choice(int_sizes), |
| 26 lambda n: 0, |
| 27 lambda n: -n, |
| 28 lambda n: n + 1, |
| 29 lambda n: n - 1, |
| 30 lambda n: n + random.randint(-1024, 1024), |
| 31 ] |
| 32 return random.choice(mutations)(n) |
| 33 |
| 34 |
| 35 def FuzzyString(s): |
| 36 """Returns a string derived from the input by one of several mutations.""" |
| 37 # First try some mutations that try to recognize certain types of strings |
| 38 try: |
| 39 s.decode("utf-8") # These mutations only make sense for textual data |
| 40 except UnicodeDecodeError: |
| 41 pass |
| 42 else: |
| 43 chained_mutations = [ |
| 44 FuzzIntsInString, |
| 45 FuzzBase64InString, |
| 46 FuzzListInString, |
| 47 ] |
| 48 original = s |
| 49 for mutation in chained_mutations: |
| 50 s = mutation(s) |
| 51 # Stop if we've modified the string and our coin comes up heads |
| 52 if s != original and random.getrandbits(1): |
| 53 return s |
| 54 |
| 55 # If we're still here, apply a more generic mutation |
| 56 mutations = [ |
| 57 lambda s: "".join(random.choice(string.printable) for i in |
| 58 xrange(utils.UniformExpoInteger(0, 14))), |
| 59 lambda s: "".join(unichr(random.randint(0, sys.maxunicode)) for i in |
| 60 xrange(utils.UniformExpoInteger(0, 14))).encode("utf-8"), |
| 61 lambda s: os.urandom(utils.UniformExpoInteger(0, 14)), |
| 62 lambda s: s * utils.UniformExpoInteger(1, 5), |
| 63 lambda s: s + "A" * utils.UniformExpoInteger(0, 14), |
| 64 lambda s: "A" * utils.UniformExpoInteger(0, 14) + s, |
| 65 lambda s: s[:-random.randint(1, max(1, len(s) - 1))], |
| 66 lambda s: textwrap.fill(s, random.randint(1, max(1, len(s) - 1))), |
| 67 lambda s: "", |
| 68 ] |
| 69 return random.choice(mutations)(s) |
| 70 |
| 71 |
| 72 def FuzzIntsInString(s): |
| 73 """Returns a string where some integers have been fuzzed with FuzzyInt.""" |
| 74 def ReplaceInt(m): |
| 75 val = m.group() |
| 76 if random.getrandbits(1): # Flip a coin to decide whether to fuzz |
| 77 return val |
| 78 if not random.getrandbits(4): # Delete the integer 1/16th of the time |
| 79 return "" |
| 80 decimal = val.isdigit() # Assume decimal digits means a decimal number |
| 81 n = FuzzyInt(int(val) if decimal else int(val, 16)) |
| 82 return str(n) if decimal else "%x" % n |
| 83 return re.sub(r"\b[a-fA-F]*\d[0-9a-fA-F]*\b", ReplaceInt, s) |
| 84 |
| 85 |
| 86 def FuzzBase64InString(s): |
| 87 """Returns a string where Base64 components are fuzzed with FuzzyBuffer.""" |
| 88 def ReplaceBase64(m): |
| 89 fb = FuzzyBuffer(base64.b64decode(m.group())) |
| 90 fb.RandomMutation() |
| 91 return base64.b64encode(fb) |
| 92 # This only matches obvious Base64 words with trailing equals signs |
| 93 return re.sub(r"(?<![A-Za-z0-9+/])" |
| 94 r"(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)" |
| 95 r"(?![A-Za-z0-9+/])", ReplaceBase64, s) |
| 96 |
| 97 |
| 98 def FuzzListInString(s, separators=r", |,|; |;|\r\n|\s"): |
| 99 """Tries to interpret the string as a list, and fuzzes it if successful.""" |
| 100 seps = re.findall(separators, s) |
| 101 if not seps: |
| 102 return s |
| 103 sep = random.choice(seps) # Ones that appear often are more likely |
| 104 items = FuzzyList(s.split(sep)) |
| 105 items.RandomMutation() |
| 106 return sep.join(items) |
| 107 |
| 108 |
| 109 class FuzzySequence(object): |
| 110 """A helpful mixin for writing fuzzy mutable sequence types. |
| 111 |
| 112 If a method parameter is left at its default value of None, an appropriate |
| 113 random value will be chosen. |
| 114 """ |
| 115 |
| 116 def Overwrite(self, value, location=None, amount=None): |
| 117 """Overwrite amount elements starting at location with value. |
| 118 |
| 119 Value can be a function of no arguments, in which case it will be called |
| 120 every time a new value is needed. |
| 121 """ |
| 122 if location is None: |
| 123 location = random.randint(0, max(0, len(self) - 1)) |
| 124 if amount is None: |
| 125 amount = utils.RandomLowInteger(min(1, len(self)), len(self) - location) |
| 126 if hasattr(value, "__call__"): |
| 127 new_elements = (value() for i in xrange(amount)) |
| 128 else: |
| 129 new_elements = itertools.repeat(value, amount) |
| 130 self[location:location+amount] = new_elements |
| 131 |
| 132 def Insert(self, value, location=None, amount=None, max_exponent=14): |
| 133 """Insert amount elements starting at location. |
| 134 |
| 135 Value can be a function of no arguments, in which case it will be called |
| 136 every time a new value is needed. |
| 137 """ |
| 138 if location is None: |
| 139 location = random.randint(0, max(0, len(self) - 1)) |
| 140 if amount is None: |
| 141 amount = utils.UniformExpoInteger(0, max_exponent) |
| 142 if hasattr(value, "__call__"): |
| 143 new_elements = (value() for i in xrange(amount)) |
| 144 else: |
| 145 new_elements = itertools.repeat(value, amount) |
| 146 self[location:location] = new_elements |
| 147 |
| 148 def Delete(self, location=None, amount=None): |
| 149 """Delete amount elements starting at location.""" |
| 150 if location is None: |
| 151 location = random.randint(0, max(0, len(self) - 1)) |
| 152 if amount is None: |
| 153 amount = utils.RandomLowInteger(min(1, len(self)), len(self) - location) |
| 154 del self[location:location+amount] |
| 155 |
| 156 |
| 157 class FuzzyList(list, FuzzySequence): |
| 158 """A list with additional methods for fuzzing.""" |
| 159 |
| 160 def RandomMutation(self, count=None, new_element=""): |
| 161 """Apply count random mutations chosen from a list.""" |
| 162 random_items = lambda: random.choice(self) if self else new_element |
| 163 mutations = [ |
| 164 lambda: random.shuffle(self), |
| 165 self.reverse, |
| 166 functools.partial(self.Overwrite, new_element), |
| 167 functools.partial(self.Overwrite, random_items), |
| 168 functools.partial(self.Insert, new_element, max_exponent=10), |
| 169 functools.partial(self.Insert, random_items, max_exponent=10), |
| 170 self.Delete, |
| 171 ] |
| 172 if count is None: |
| 173 count = utils.RandomLowInteger(1, 5, beta=3.0) |
| 174 for _ in xrange(count): |
| 175 random.choice(mutations)() |
| 176 |
| 177 |
| 178 class FuzzyBuffer(bytearray, FuzzySequence): |
| 179 """A bytearray with additional methods for mutating the sequence of bytes.""" |
| 180 |
| 181 def __repr__(self): |
| 182 return "%s(%r)" % (self.__class__.__name__, str(self)) |
| 183 |
| 184 def FlipBits(self, num_bits=None): |
| 185 """Flip num_bits bits in the buffer at random.""" |
| 186 if num_bits is None: |
| 187 num_bits = utils.RandomLowInteger(min(1, len(self)), len(self) * 8) |
| 188 for bit in random.sample(xrange(len(self) * 8), num_bits): |
| 189 self[bit / 8] ^= 1 << (bit % 8) |
| 190 |
| 191 def RandomMutation(self, count=None): |
| 192 """Apply count random mutations chosen from a weighted list.""" |
| 193 random_bytes = lambda: random.randint(0x00, 0xFF) |
| 194 mutations = [ |
| 195 (self.FlipBits, 1), |
| 196 (functools.partial(self.Overwrite, random_bytes), 1/3.0), |
| 197 (functools.partial(self.Overwrite, 0xFF), 1/3.0), |
| 198 (functools.partial(self.Overwrite, 0x00), 1/3.0), |
| 199 (functools.partial(self.Insert, random_bytes), 1/3.0), |
| 200 (functools.partial(self.Insert, 0xFF), 1/3.0), |
| 201 (functools.partial(self.Insert, 0x00), 1/3.0), |
| 202 (self.Delete, 1), |
| 203 ] |
| 204 if count is None: |
| 205 count = utils.RandomLowInteger(1, 5, beta=3.0) |
| 206 for _ in xrange(count): |
| 207 utils.WeightedChoice(mutations)() |
| OLD | NEW |