Source code for boofuzz.primitives.bytes

"""
:mod:`boofuzz.primitives.bytes`
================================
This module contains the implementation of the Bytes primitive.
"""
from collections.abc import ByteString
import os
import itertools
import math
import random

from funcy import compose
from .base_primitive import BasePrimitive


[docs] class Bytes(BasePrimitive): """Primitive that fuzzes a binary byte string with arbitrary length. :type name: str, optional :param name: Name, for referencing later. Names should always be provided, but if not, * a default name will be given, defaults to None :type default_value: bytes, optional :param default_value: Value used when the element is not being fuzzed - should typically represent a valid value, defaults to b"" :type size: int, optional :param size: Deprecated, kept for retrocompatibility, use min_len and max_len. Static size of this field, leave None for dynamic, defaults to None :type padding: chr, optional :param padding: Value to use as padding to fill static field size, defaults to b"\\x00" :type min_len: int, optional :param min_len: Minimum string length, defaults to 0 :type max_len: int, optional :param max_len: Maximum string length, defaults to None :type max_element_by_round: int, optional :param max_element_by_round: Maximum number of elements to generate by round, defaults to 100 Truncate seclists if necessary :type fuzzable: bool, optional :param fuzzable: Enable/disable fuzzing of this primitive, defaults to true :type use_long_bytes: bool, optional :param use_long_bytes: Enable/disable the use of long bytes, defaults to True :type use_default_value: bool, optional :param use_default_value: Enable/disable the use of the default value, defaults to True """ # from https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_debug_values _magic_debug_values = [ b"\x00", b"\x01", b"\x80", b"\xa5", b"\xFF", b"\x01\x00", b"\x00\x01", b'\xba\xd2""', b"\x7F\xFF", b"\xFF\x7F", b"\xFE\xFF", b"\xFF\xFE", b"\r\x15\xea^", b"\x00\x00\x81#", b"\xb1k\x00\xb5", b"\xba\xad\xf0\r", b"\x8b\xad\xf0\r", b"\xde\xad\xf0\r", b"\xca\xfe\xd0\r", b"\x00\x00\x00\x01", b"\x01\x00\x00\x00", b"\x7F\xFF\xFF\xFF", b"\xFF\xFF\xFF\x7F", b"\xFE\xFF\xFF\xFF", b"\xFF\xFF\xFF\xFE", b"\x00\xfa\xca\xde", b"\x1b\xad\xb0\x02", b"\xa5\xa5\xa5\xa5", b"\xab\xab\xab\xab", b"\xab\xad\xba\xbe", b"\xab\xba\xba\xbe", b"\xab\xad\xca\xfe", b"\xba\xaa\xaa\xad", b"\xba\xdd\xca\xfe", b"\xbb\xad\xbe\xef", b"\xbe\xef\xca\xce", b"\xc0\x00\x10\xff", b"\xca\xfe\xba\xbe", b"\xca\xfe\xfe\xed", b"\xcc\xcc\xcc\xcc", b"\xcd\xcd\xcd\xcd", b"\xdd\xdd\xdd\xdd", b"\xde\xad\x10\xcc", b"\xde\xad\xba\xbe", b"\xde\xad\xbe\xef", b"\xde\xad\xca\xfe", b"\xde\xad\xc0\xde", b"\xde\xad\xfa\x11", b"\xde\xfe\xc8\xed", b"\xde\xad\xde\xad", b"\xeb\xeb\xeb\xeb", b"\xfa\xde\xde\xad", b"\xfd\xfd\xfd\xfd", b"\xfe\xe1\xde\xad", b"\xfe\xed\xfa\xce", b"\xfe\xee\xfe\xee", b"\xba\xdb\xad\xba\xdb\xad", b"\xba\xdc\x0f\xfe\xe0\xdd\xf0\r", ] _long_bytes_lengths = [8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 32768, 0xFFFF] _long_bytes_deltas = [-2, -1, 0, 1, 2] _extra_long_bytes_lengths = [99999, 100000, 500000, 1000000] _default_value_multipliers = [2, 10, 100] def __init__( self, *args, name: str = None, default_value: bytes = b"", padding: bytes = b"\x00", size: int = None, min_len:int=0, max_len:int=1000, use_long_bytes:bool=True, use_default_value: bool = True, **kwargs ): # Check types if not isinstance(default_value, ByteString): raise TypeError("default_value of Bytes must be of ByteString type") if not isinstance(padding, ByteString): raise TypeError("padding of Bytes must be of ByteString type") super().__init__(name=name, default_value=default_value, *args, **kwargs) self.min_len = min_len self.max_len = max_len # Keeping self.size for retrocompatibility self.size = size self.random_indices = {} self.use_long_bytes=use_long_bytes self.use_default_value = use_default_value if self.size is not None: self.max_len = self.size self.min_len = self.size self.padding = padding
[docs] def mutations(self, default_value): """ Generate mutations : * If the round_type is "library", do so using the variables and functions yielded by _iterate_fuzz_cases. * If the element is callable, calls it. * If not, returns the element. * Checks for the size each time. * Else, if round_type is random_mutation, flip random bits in the value. * Else, if round_type is random_generation, generate random bytes of random size. :param default_value: :return: A generator of mutated values """ if self.request.parent_session.round_type == "library" : for fuzz_value in itertools.islice(itertools.chain( self._yield_variable_mutations(default_value), self._yield_long_magic_debug_values(), self._yield_from_file(), ), self.num_library_elements): if callable(fuzz_value): yield compose(self._adjust_mutation_for_size, fuzz_value) else: yield self._adjust_mutation_for_size(fuzz_value=fuzz_value) if self.request.parent_session.round_type == "random_mutation" : # Get all the values from the itertools.chain library = list(itertools.chain( self._yield_variable_mutations(default_value), self._yield_long_magic_debug_values(), self._yield_from_file(), )) # If the seed index (the round number) is less than or equal to the max_rounds_mutation, # mutate the character if self.request.parent_session.seed_index < self.max_rounds_mutation: # Get the seedth value of the itertools.chain # seed isn't only used to generate random, but also as an index current_val = self.get_nth(library, self.request.parent_session.seed_index) # If the current value is not None, yield the mutated character. # If the current value is not None, yield the mutated character. # If it is None, do nothing. random.seed(self.primitive_seed) if current_val is not None: for data in self._mutate_bytes(current_val): yield self._adjust_mutation_for_size(data) if self.request.parent_session.round_type == "random_generation": random.seed(self.primitive_seed) for _ in range(self.num_random_generations): yield self.random_generation()
def _adjust_mutation_for_size(self, fuzz_value:bytes): """ If the fuzz_value is too long, cut it. If it is too short, pad it. :type fuzz_value: bytes :param fuzz_value: The fuzz_value to adjust :return: The adjusted fuzz_value """ if len(fuzz_value) > self.max_len: fuzz_value = fuzz_value[: self.max_len] if len(fuzz_value) < self.min_len: fuzz_value = fuzz_value + self.padding * (self.min_len - len(fuzz_value)) return fuzz_value def _yield_variable_mutations(self, default_value): """ Yield variable mutations of the default value if use_default_value is True. """ if self.use_default_value: for length in self._default_value_multipliers: value = default_value * length yield value if self.max_len is not None and len(value) >= self.max_len: break yield value if self.max_len is not None and len(value) >= self.max_len: break else : yield default_value def _yield_from_file(self): """ Load fuzz library from file. Yields lines from file that are not comments or empty. Ignore if seclist_path is empty. Raises : FileNotFoundError if file not found. """ if self.seclist_path : abs_filepath = self._get_seclist_abs_path() # Open file and yield lines that are not comments or empty try: with open(abs_filepath, "r", encoding="ascii") as f: for line in f: line = line.strip() if line and not line.startswith("#"): yield line.encode() except FileNotFoundError as exc: raise FileNotFoundError(f"File not found: {abs_filepath}") from exc def _yield_long_magic_debug_values(self): """ For each value in magic_debug_values, yield a number of selectively chosen bytes lengths. Ignore if use_long_bytes is False. """ if self.use_long_bytes: for sequence in self._magic_debug_values: if len(sequence)!=0: for size in [ length + delta for length, delta in itertools.product(self._long_bytes_lengths, self._long_bytes_deltas) ]: if self.max_len is None or size <= self.max_len: data = sequence * math.ceil(size / len(sequence)) yield data[:size] else: break for size in self._extra_long_bytes_lengths: if self.max_len is None or size <= self.max_len: data = sequence * math.ceil(size / len(sequence)) yield data[:size] else: break if self.max_len is not None: data = sequence * math.ceil(self.max_len / len(sequence)) yield data def _mutate_bytes(self, bytes_to_mutate: bytes) : for _ in range(self.num_random_mutations): if bytes_to_mutate == "": yield bytes_to_mutate # Convert the byte string to a list of bits bit_list = list(format(int.from_bytes(bytes_to_mutate, 'big'), '08b')) # Choose a random bit to flip bit_to_flip = random.randint(0, len(bit_list) - 1) # Flip the chosen bit bit_list[bit_to_flip] = '0' if bit_list[bit_to_flip] == '1' else '1' # Convert the bit list back to a byte string bytes_to_mutate = int(''.join(bit_list), 2).to_bytes(len(bytes_to_mutate), 'big') # Replace the character at the chosen position with the new character yield bytes_to_mutate
[docs] def num_mutations(self, default_value) -> int: """ Calculate and return the total number of mutations for this individual primitive. @rtype: int @return: Number of mutated forms this primitive can take :param default_value: """ if self.request.parent_session.round_type == "library" : return len( list( itertools.islice( itertools.chain( self._yield_variable_mutations(default_value), self._yield_long_magic_debug_values(), self._yield_from_file(), ), self.num_library_elements ) ) ) if self.request.parent_session.round_type == "random_mutation": return self.num_random_mutations if self.request.parent_session.round_type == "random_generation": return self.num_random_generations
[docs] def encode(self, value, mutation_context): if value is None: value = b"" return value
[docs] def random_generation(self): """ Generate random bytes, of a random size between self.min_len and self.max_len. """ size = random.randint(self.min_len, self.max_len) return os.urandom(size)