################################################################################ # Name : BinaryFuzz.py # Author : Tyson Smith & Jesse Schwartzentruber # # Copyright 2014 BlackBerry Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ################################################################################ import os import random from . import file_fixer ################################################################################ # Globals ################################################################################ # Type of fuzzing to perform # single byte operations BINFUZZ_RANDOM = 0 BINFUZZ_INC = 1 BINFUZZ_DEC = 2 BINFUZZ_ZERO = 3 BINFUZZ_MAX = 4 BINFUZZ_SPECIAL = 5 BINFUZZ_ONE = 6 BINFUZZ_NEGATE = 7 BINFUZZ_XOR = 8 BINFUZZ_BOUNDARY = 9 BINFUZZ_ALTERNATE = 10 # multi byte operations BINFUZZ_SWAP = 11 BINFUZZ_DUP = 12 BINFUZZ_CORRUPT = 13 BINFUZZ_CORRUPT_INPLACE = 14 BINFUZZ_CHOP = 15 # add new BINFUZZ types above, and update count below BINFUZZ_N = 16 SINGLE_BYTE_OPS = 10 # up to and including SPECIAL_BINFUZZ_VALUE = 0x41 ################################################################################ # Functions ################################################################################ def random_binfuzz_type(): """ This function will return a randomly chosen fuzz_type from ``BINFUZZ_*``. """ return randint(0, BINFUZZ_N-1) # takes an integer argument, returns an integer def _mutate_byte(b, fuzz_type, special=SPECIAL_BINFUZZ_VALUE): """ This function applies the given *fuzz_type* to the input byte, *b*. *fuzz_type* is one of the ``BINFUZZ_*`` constants. The result is a fuzzed byte value. Both input and result are integer values between 0 and 255 inclusive. """ if fuzz_type == BINFUZZ_RANDOM: result = random.randint(0, 255) elif fuzz_type == BINFUZZ_INC: result = b + random.randint(1, 10) if random.randint(1, 10) == 1 else b + 1 elif fuzz_type == BINFUZZ_DEC: result = b - random.randint(1, 10) if random.randint(1, 10) == 1 else b - 1 elif fuzz_type == BINFUZZ_ZERO: result = 0 elif fuzz_type == BINFUZZ_MAX: result = 0xFF elif fuzz_type == BINFUZZ_SPECIAL: result = special elif fuzz_type == BINFUZZ_ONE: result = 0x1 elif fuzz_type == BINFUZZ_NEGATE: result = ~b elif fuzz_type == BINFUZZ_XOR: result = b ^ random.choice((0x11, 0x22, 0x33, 0x44, 0x55, 0x88, 0x99, 0xAA, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80)) elif fuzz_type == BINFUZZ_BOUNDARY: result = (2**random.randint(0, 7)) + random.randint(-2, 0) elif fuzz_type == BINFUZZ_ALTERNATE: result = random.choice((0xAA, 0x55, 0x0A, 0x05, 0xA0, 0x50, 0x0F, 0xF0)) else: assert False, "Unhandled Fuzz Type: %d" % fuzz_type return result & 0xFF def _mutate_bytes(target_data, special=SPECIAL_BINFUZZ_VALUE): """ This function applies :func:`~alf.fuzz._mutate_byte` to each byte in a given list :func:`~_mutate_bytes` returns a list of fuzzed bytes """ return bytearray(_mutate_byte(b, random.randint(0, SINGLE_BYTE_OPS), special) for b in target_data) ################################################################################ # Classes ################################################################################ class BinaryFileFuzzer(object): """ This class is designed to mutate a string of binary data. *fuzz_type* can be one of the ``BINFUZZ_*`` constants, or ``None`` to select at random for each mutation. *special* will override the default value of 'A' used when :data:`BINFUZZ_SPECIAL` is selected. When *fuzz_type* is None, specific fuzz types can be disabled using :meth:`~disable_fuzz_type`. *max_corrupt* is used to set the maximum amount of data to add/corrupt in a single pass. """ def __init__(self, fuzz_type=None, special=None, max_corrupt=10240): if fuzz_type is None: self.fuzz_types = list(range(BINFUZZ_N)) else: self._validate_fuzz_type(fuzz_type) self.fuzz_types = [fuzz_type] self.active_fuzz_types = list(self.fuzz_types) if special is None: special = SPECIAL_BINFUZZ_VALUE self.special = special if not isinstance(max_corrupt, int): raise TypeError("max_corrupt must be an int not %s" % type(max_corrupt).__name__) self.max_corrupt = max_corrupt @staticmethod def _validate_fuzz_type(fuzz_type): if not isinstance(fuzz_type, int): raise TypeError("fuzz_type must be an int not %s" % type(fuzz_type).__name__) if not (fuzz_type == int(fuzz_type) and 0 <= fuzz_type < BINFUZZ_N): raise ValueError("Unknown BinaryFileFuzzer fuzz type: %r" % str(fuzz_type)) def disable_fuzz_type(self, fuzz_type): """ This method disables the given *fuzz_type* for all future mutations. """ del self.fuzz_types[self.fuzz_types.index(fuzz_type)] self.active_fuzz_types = list(self.fuzz_types) def set_special_value(self, special): """ This method will set a new special value to be used by :data:`BINFUZZ_SPECIAL`. """ self.special = special def _select_active_fuzz_types(self): """ This method is used to randomly disable different fuzz types on a per iteration basis. """ type_count = len(self.fuzz_types) if type_count < 2: return self.active_fuzz_types = random.sample(self.fuzz_types, random.randint(1, type_count)) def _random_fuzz_type(self): """ This function will return a randomly chosen fuzz type from ``BINFUZZ_*``. Any fuzz types disabled by :meth:`~disable_fuzz_type` will be excluded. """ return random.choice(self.active_fuzz_types) def fuzz_data(self, file_data, aggression, fuzz_type=None, fix_ext=None): """ This method mutates the given input data using the *aggression* to determine how much of the data to mutate. Two modes of aggression are supported. If *aggression* is greater than 0, that number is the inverse of the number of bytes to mutate as a ratio of the input length (ie. filesize/aggression == number of bytes to mutate). Note that in this mode, aggression is statistical, so even an *aggression* of 1 will not hit every byte in the input. The other mode of aggression is when *aggression* is less than 0. In this case, the magnitude is used as the absolute number of bytes to mutate (ie. -1 == 1 mutation, -2 == 2 mutations, etc.). *fuzz_type* specifies how bytes will be mutated. A value of None indicates that a random fuzz_type should be used for each byte mutated. *fix_ext* is the optional extension of the original file, in which case the data will be filtered through :func:`~alf.fuzz.auto_fix` after mutation. :meth:`~fuzz_data` returns a tuple containing the number of bytes mutated, and the string of mutated binary data. """ if not isinstance(aggression, int): raise TypeError("aggression must be an int not %s" % type(aggression).__name__) if not aggression: return 0, file_data if fuzz_type is not None: self._validate_fuzz_type(fuzz_type) file_len = len(file_data) if aggression > 0: bytes_to_fuzz = max(file_len // aggression, 1) else: bytes_to_fuzz = -aggression if file_len < 1: bytes_to_fuzz = 0 bytes_fuzzed = 0 fuzzed_data = bytearray(file_data) req_fuzz_type = fuzz_type if req_fuzz_type is None: self._select_active_fuzz_types() while bytes_fuzzed < bytes_to_fuzz: work_left = bytes_to_fuzz - bytes_fuzzed if req_fuzz_type is None: fuzz_type = self._random_fuzz_type() addr = random.randint(0, file_len-1) if fuzz_type == BINFUZZ_SWAP: if bytes_to_fuzz == 1: # This copies one byte over another. fuzzed_data[addr] = fuzzed_data[random.randint(0, file_len-1)] bytes_fuzzed += 1 else: # This swaps two chunks. The number of bytes fuzzed is twice the chunk size. addr2 = random.randint(0, file_len-1) addr_low = min(addr, addr2) addr_high = max(addr, addr2) chunk_size = min(addr_high-addr_low, file_len-addr_high, 1+work_left/2) # pylint: disable=W0311 ( fuzzed_data[addr_low:addr_low+chunk_size], fuzzed_data[addr_high:addr_high+chunk_size] ) = ( fuzzed_data[addr_high:addr_high+chunk_size], fuzzed_data[addr_low:addr_low+chunk_size] ) bytes_fuzzed += 2 * chunk_size elif fuzz_type == BINFUZZ_DUP: n = min(self.max_corrupt, file_len - addr, bytes_to_fuzz - bytes_fuzzed) n = random.randint(1, random.randint(1, n)) # favor smaller numbers fuzzed_data[addr:addr] = fuzzed_data[addr:addr+n] bytes_fuzzed += n elif fuzz_type == BINFUZZ_CORRUPT: # WARNING: this can modify the file size n = min(self.max_corrupt, bytes_to_fuzz - bytes_fuzzed) n = random.randint(1, random.randint(1, n)) # favor smaller numbers fuzzed_data[addr:addr] = os.urandom(n) bytes_fuzzed += n elif fuzz_type == BINFUZZ_CORRUPT_INPLACE: n = min(self.max_corrupt, file_len - addr, bytes_to_fuzz - bytes_fuzzed) if n > 2: n = random.randint(1, random.randint(2, n)) # favor smaller numbers fuzzed_data[addr:addr+n] = _mutate_bytes(fuzzed_data[addr:addr+n], self.special) bytes_fuzzed += n elif fuzz_type == BINFUZZ_CHOP: # WARNING: this can modify the file size n = min(file_len - addr, bytes_to_fuzz - bytes_fuzzed, 64) n = random.randint(1, random.randint(1, n)) # favor smaller numbers del fuzzed_data[addr:addr+n] bytes_fuzzed += n file_len -= n else: fuzzed_data[addr] = _mutate_byte(fuzzed_data[addr], fuzz_type, special=self.special) bytes_fuzzed += 1 if fix_ext is not None: fuzzed_data = file_fixer.auto_fixer(fuzzed_data, fix_ext) return bytes_fuzzed, fuzzed_data