Source code for boofuzz.primitives.bit_field

import struct

from .. import helpers
from ..constants import LITTLE_ENDIAN
from ..fuzzable import Fuzzable


def binary_string_to_int(binary):
    """
    Convert a binary string to a decimal number.

    @type  binary: str
    @param binary: Binary string

    @rtype:  int
    @return: Converted bit string
    """

    return int(binary, 2)


def int_to_binary_string(number, bit_width):
    """
    Convert a number to a binary string.

    @type  number:    int
    @param number:    (Optional, def=self._value) Number to convert
    @type  bit_width: int
    @param bit_width: (Optional, def=self.width) Width of bit string

    @rtype:  str
    @return: Bit string
    """
    return "".join(map(lambda x: str((number >> x) & 1), range(bit_width - 1, -1, -1)))


[docs] class BitField(Fuzzable): """ The bit field primitive represents a number of variable length and is used to define all other integer types. :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: int, optional :param default_value: Default integer value, defaults to 0 :type width: int, optional :param width: Width in bits, defaults to 8 :type max_num: int, optional :param max_num: Maximum number to iterate up to, defaults to None :type endian: char, optional :param endian: Endianness of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >), defaults to LITTLE_ENDIAN :type output_format: str, optional :param output_format: Output format, "binary" or "ascii", defaults to binary :type signed: bool, optional :param signed: Make size signed vs. unsigned (applicable only with format="ascii"), defaults to False :type full_range: bool, optional :param full_range: If enabled the field mutates through *all* possible values, defaults to False :type fuzz_values: list, optional :param fuzz_values: List of custom fuzz values to add to the normal mutations, defaults to None :type fuzzable: bool, optional :param fuzzable: Enable/disable fuzzing of this primitive, defaults to true """ def __init__( self, name=None, default_value=0, width=8, max_num=None, endian=LITTLE_ENDIAN, output_format="binary", signed=False, full_range=False, *args, **kwargs ): super(BitField, self).__init__(name=name, default_value=default_value, *args, **kwargs) assert isinstance(width, int), "width must be an integer!" self.width = width self.max_num = max_num self.endian = endian self.format = output_format self.signed = signed self.full_range = full_range if not self.max_num: self.max_num = binary_string_to_int("1" + "0" * width) assert isinstance(self.max_num, int), "max_num must be an integer!" if not self.full_range: # try only "smart" values. interesting_boundaries = [ 0, self.max_num // 2, self.max_num // 3, self.max_num // 4, self.max_num // 8, self.max_num // 16, self.max_num // 32, self.max_num, ] # Contract: sort in ascending order required for deduplication. interesting_boundaries.sort() self._interesting_boundaries = interesting_boundaries else: self._interesting_boundaries = [] def _iterate_fuzz_lib(self): if self.full_range: for i in range(0, self.max_num): yield i else: # To avoid duplication of mutatation values, we introduce gradually # increasing lower border. Combined with interesting boundaries # sorted in ascending order, it's possible to avoid duplicates. # Deduplication could also be done with intermediate 'set' construction, # but it might be costly or impossible if the number of values is huge. # We don't expect negative bit field values with Python integers, # the 0-value for mutation is possible with the given starting 'lower_border'. lower_border = -1 for boundary in self._interesting_boundaries: for v in self._yield_integer_boundaries(boundary, lower_border): lower_border = v yield v # TODO Add a way to inject a list of fuzz values # elif isinstance(default_value, (list, tuple)): # for val in iter(default_value): # yield val # TODO: Add injectable arbitrary bit fields def _yield_integer_boundaries(self, integer, lower_border): """ Add the supplied integer and border cases to the integer fuzz heuristics library. @type integer: int @param integer: int to append to fuzz heuristics @type lower_border: int @param lower_border: int bottom limit for border cases, so all values must be strictly greater """ # Contract: generate values in ascending order, otherwise deduplication logics might break. for i in range(-10, 10): case = integer + i if lower_border < case < self.max_num: # some day: if case not in self._user_provided_values yield case def encode(self, value, mutation_context): temp = self._render_int( value, output_format=self.format, bit_width=self.width, endian=self.endian, signed=self.signed ) return helpers.str_to_bytes(temp) def mutations(self, default_value): for val in self._iterate_fuzz_lib(): yield val @staticmethod def _render_int(value, output_format, bit_width, endian, signed): """ Convert value to a bit or byte string. Args: value (int): Value to convert to a byte string. output_format (str): "binary" or "ascii" bit_width (int): Width of output in bits. endian: BIG_ENDIAN or LITTLE_ENDIAN signed (bool): Returns: str: value converted to a byte string """ if output_format == "binary": bit_stream = "" rendered = b"" # pad the bit stream to the next byte boundary. if bit_width % 8 == 0: bit_stream += int_to_binary_string(value, bit_width) else: bit_stream = "0" * (8 - (bit_width % 8)) bit_stream += int_to_binary_string(value, bit_width) # convert the bit stream from a string of bits into raw bytes. for i in range(len(bit_stream) // 8): chunk_min = 8 * i chunk_max = chunk_min + 8 chunk = bit_stream[chunk_min:chunk_max] rendered += struct.pack("B", binary_string_to_int(chunk)) # if necessary, convert the endianness of the raw bytes. if endian == LITTLE_ENDIAN: # reverse the bytes rendered = rendered[::-1] _rendered = rendered else: # Otherwise we have ascii/something else # if the sign flag is raised and we are dealing with a signed integer (first bit is 1). if signed and int_to_binary_string(value, bit_width)[0] == "1": max_num = binary_string_to_int("1" + "0" * (bit_width - 1)) # chop off the sign bit. val = value & binary_string_to_int("1" * (bit_width - 1)) # account for the fact that the negative scale works backwards. val = max_num - val - 1 # toss in the negative sign. _rendered = "%d" % ~val # unsigned integer or positive signed integer. else: _rendered = "%d" % value return _rendered