def __init__(self, field, comp_type, comp_value): ''' :param field: (name of) field that should meet the condition :param comp_type: how to compare the values. One of ('>', '<', '>=', '<=', '==', '!=') :param comp_value: value to compare the field to :example: Render the content of the `If` container only if the value in the field called "name" is not 'kitty' :: Template([ String('kitty', name='name'), If(Compare('name', '!=', 'kitty'), [ String('current name is not kitty') ]) ]) ''' super(Compare, self).__init__(field=field) self._comp_type = comp_type if comp_type in Compare._comparison_types: if comp_type not in ['==', '!='] and isinstance(comp_value, str): raise KittyException( 'can\'t use comparison type "%s" with comparison value of type str' % comp_type) self._comp_fn = Compare._comparison_types[comp_type] else: raise KittyException('unknown comparison type (%s)' % (comp_type)) if isinstance(comp_value, six.string_types): comp_value = strToBytes(comp_value) self._comp_value = comp_value
def __init__(self, field, comp_type, comp_value): ''' :param field: (name of) field that should meet the condition :param comp_type: how to compare the values. One of ('>', '<', '>=', '<=', '==', '!=') :param comp_value: value to compare the field to :example: :: Template([ String(['kitty'], name='name'), If(Compare('name', '!=', 'kitty'), [ String('123') ]) ]) ''' super(Compare, self).__init__(field=field) self._comp_type = comp_type if comp_type in Compare._comparison_types: if comp_type not in ['==', '!='] and isinstance(comp_value, str): raise KittyException( 'can\'t use comparison type "%s" with comparison value of type str' % comp_type) self._comp_fn = Compare._comparison_types[comp_type] else: raise KittyException('unknown comparison type (%s)' % (comp_type)) self._comp_value = comp_value
def url_from_string(url, fuzz_delims=True, fuzzable=True, name=None): ''' Create a URL from string, only URLs with supported schemes will result in a lego. In the rest of the cases, an exception will be raised. :param url: the URL string :param fuzz_delims: should fuzz delimiters (default: True) :param fuzzable: should the resulted container be fuzzable (default: True) :param name: name of the resulted container (default: None) ''' generators = { 'http': HttpUrl.from_string, 'https': HttpUrl.from_string, 'ftp': FtpUrl.from_string, 'ftps': FtpUrl.from_string, 'mailto': EmailUrl.from_string, } parsed = urlparse(url) scheme = parsed.scheme if not scheme: raise KittyException('URL is invalid (no scheme)') if scheme in generators: generator = generators[scheme] return generator(the_url=url, fuzz_delims=fuzz_delims, fuzzable=fuzzable, name=name) else: raise KittyException('Unknown URL scheme (%s)' % scheme)
def __init__(self, value, num_bytes=1, fuzzable=True, name=None): ''' :type value: str or bytes :param value: value to mutate :param num_bytes: number of consequtive bytes to flip (invert) :param fuzzable: is field fuzzable (default: True) :param name: name of the object (default: None) :raises: ``KittyException`` if num_bytes is bigger than the value length :raises: ``KittyException`` if num_bytes is not positive ''' kassert.is_of_types(value, (bytes, bytearray, str)) value = strToBytes(value) if len(value) < num_bytes: raise KittyException('len(value) <= num_bytes', (len(value), num_bytes)) if num_bytes <= 0: raise KittyException('num_bytes(%d) <= 0' % (num_bytes)) super(ByteFlip, self).__init__(value=value, encoder=ENC_STR_DEFAULT, fuzzable=fuzzable, name=name) self._data_len = len(value) self._num_bytes = num_bytes self._num_mutations = self._data_len - (num_bytes - 1)
def _get_ready(self): if not self._ready: if re.match(Stage._random_pattern, self._strategy): self._min_sequence = 1 self._max_sequence = len(self._templates) elif re.match(Stage._all_pattern, self._strategy): self._min_sequence = len(self._templates) self._max_sequence = len(self._templates) elif re.match(Stage._const_pattern, self._strategy): self._min_sequence = int(self._strategy) self._max_sequence = int(self._strategy) if self._max_sequence > len(self._templates): raise KittyException( 'bad const strategy %s > template count(%s)' % (self._max_sequence, len(self._templates))) elif re.match(Stage._range_pattern, self._strategy): ts = self._strategy.split('-') self._min_sequence = int(ts[0]) self._max_sequence = int(ts[1]) if self._max_sequence < self._min_sequence: raise KittyException('bad range strategy %s, max < min' % self._strategy) if self._max_sequence > len(self._templates): raise KittyException( 'bad range strategy %s, max > template count(%s)' % (self._max_sequence, len(self._templates))) self._ready = True
def _calc_bounds(self, value, minv, maxv): if self._length <= 0: raise KittyException('length (%d) <= 0' % (self._length)) max_possible = 2**self._length - 1 if self._signed: self._min_value = ~(max_possible >> 1) else: self._min_value = 0 self._max_value = max_possible + self._min_value self._max_min_diff = max_possible if maxv is not None: if maxv > self._max_value: raise KittyException('max_value is too big %d > %d' % (maxv, self._max_value)) self._max_value = maxv if minv is not None: if minv < self._min_value: raise KittyException('min_value is too small %d < %d' % (minv, self._min_value)) self._min_value = minv if self._min_value > self._max_value: raise KittyException('min_value (%d) > max_value (%d)' % (self._min_value, self._max_value)) if (value < self._min_value) or (value > self._max_value): raise KittyException( 'default value (%d) not in range (min=%d, max=%d)' % (value, self._min_value, self._max_value))
def _parse(self): ''' Crazy function to check and parse the range list string ''' if not self._ranges_str: self._lists = [StartEndList(0, None)] else: lists = [] p_single = re.compile(r'(\d+)$') p_open_left = re.compile(r'-(\d+)$') p_open_right = re.compile(r'(\d+)-$') p_closed = re.compile(r'(\d+)-(\d+)$') for entry in self._ranges_str.split(','): entry = entry.strip() # single number match = p_single.match(entry) if match: num = int(match.groups()[0]) lists.append(StartEndList(num, num + 1)) continue # open left match = p_open_left.match(entry) if match: end = int(match.groups()[0]) lists.append(StartEndList(0, end + 1)) continue # open right match = p_open_right.match(entry) if match: start = int(match.groups()[0]) self._open_end_start = start lists.append(StartEndList(start, None)) continue # closed range match = p_closed.match(entry) if match: start = int(match.groups()[0]) end = int(match.groups()[1]) lists.append(StartEndList(start, end + 1)) continue # invalid expression raise KittyException('Invalid range found: %s' % entry) lists = sorted(lists, key=lambda x: x._start) for i in range(len(lists) - 1): if lists[i]._end is None: # there is an open end which is not the last in our lists # this is a clear overlap with the last one ... raise KittyException('Overlapping ranges in range list') elif lists[i]._end > lists[i + 1]._start: raise KittyException('Overlapping ranges in range list') self._lists = lists
def _validate_lengths(self, min_length, max_length): kassert.is_int(min_length) kassert.is_int(max_length) if min_length > max_length: raise KittyException('min_length(%d) > max_length(%d)' % (min_length, max_length)) elif min_length < 0: raise KittyException('min_length(%d) < 0' % (min_length)) elif max_length <= 0: raise KittyException('max_length(%d) < 0' % (max_length))
def __init__(self, value, min_length, max_length, unused_bits=0, seed=1235, num_mutations=25, step=None, encoder=ENC_BITS_DEFAULT, fuzzable=True, name=None): ''' :type value: str :param value: default value, the last *unsused_bits* will be removed from the value :param min_length: minimal length of the field (in bits) :param max_length: maximal length of the field (in bits) :param unused_bits: how many bits from the value are not used (default: 0) :param seed: seed for the random number generator, to allow consistency between runs (default: 1235) :param num_mutations: number of mutations to perform (if step is None) (default:25) :type step: int :param step: step between lengths of each mutation (default: None) :type encoder: :class:`~kitty.model.low_level.encoder.BitsEncoder` :param encoder: encoder for the field (default: ENC_BITS_DEFAULT) :param fuzzable: is field fuzzable (default: True) :param name: name of the object (default: None) :examples: :: RandomBits(value='1234', min_length=0, max_length=75, unused_bits=0, step=15) RandomBits(value='1234', min_length=0, max_length=75, unused_bits=3, num_mutations=80) ''' if unused_bits not in range(8): raise KittyException('unused bits (%d) is not between 0-7' % unused_bits) value = Bits(bytes=value) if unused_bits: value = value[:-unused_bits] super(RandomBits, self).__init__(value=value, encoder=encoder, fuzzable=fuzzable, name=name) self._validate_lengths(min_length, max_length) self._min_length = min_length self._max_length = max_length self._num_mutations = num_mutations self._step = step self._random = Random() self._seed = seed self._random.seed(self._seed) if self._step: if self._step < 0: raise KittyException('step (%d) < 0' % (step)) self._num_mutations = (self._max_length - self._min_length) / self._step
def _get_ready(self, container): if not self._field: if self._field_name: field = container.resolve_field(self._field_name) if not field: raise KittyException('failed to resolve field name %s' % self._field_name) self._field = field if not self._field: raise KittyException('No field provided to base the condition on')
def _check_session_validity(self): current_version = _get_current_version() if current_version != self.session_info.kitty_version: raise KittyException( 'kitty version in stored session (%s) != current kitty version (%s)' % (current_version, self.session_info.kitty_version)) model_hash = self.model.hash() if model_hash != self.session_info.data_model_hash: raise KittyException( 'data model hash in stored session(%s) != current data model hash (%s)' % (model_hash, self.session_info.data_model_hash))
def __init__(self, value, min_length, max_length, seed=1234, num_mutations=25, step=None, encoder=ENC_STR_DEFAULT, fuzzable=True, name=None): ''' :type value: str :param value: default value :param min_length: minimal length of the field (in bytes) :param max_length: maximal length of the field (in bytes) :param seed: seed for the random number generator, to allow consistency between runs (default: 1234) :param num_mutations: number of mutations to perform (if step is None) (default:25) :type step: int :param step: step between lengths of each mutation (default: None) :type encoder: :class:`~kitty.model.low_level.encoder.StrEncoder` :param encoder: encoder for the field (default: ENC_STR_DEFAULT) :param fuzzable: is field fuzzable (default: True) :param name: name of the object (default: None) :examples: :: RandomBytes(value='1234', min_length=0, max_length=75, step=15) RandomBytes(value='1234', min_length=0, max_length=75, num_mutations=80) ''' super(RandomBytes, self).__init__(value=value, encoder=encoder, fuzzable=fuzzable, name=name) self._validate_lengths(min_length, max_length) self._min_length = min_length self._max_length = max_length self._num_mutations = num_mutations self._step = step self._random = Random() self._seed = seed self._random.seed(self._seed) if self._step: if self._step < 0: raise KittyException('step (%d) < 0' % (step)) self._num_mutations = (self._max_length - self._min_length) // self._step
def __init__(self, name, device, baudrate=115200, timeout=0.5, open_at='setup', logger=None, expect_response=False): ''' :param name: name of the target :param device: serial device name/path :param baudrate: baud rate of the serial channel (default: 115200) :param timeout: receive timeout on the channel in seconds (default: 0.5) :type open_at: str :param open_at: at what stage should the port be opened. Either 'setup' or 'pre_test' (default: 'setup') :param logger: logger for this object (default: None) :param expect_response: should wait for response from the victim (default: False) :examples: >>> SerialTarget('SomeTarget', '/dev/ttyUSB0', 57600) >>> SerialTarget('ToTarget', '/dev/ttyUSB0', timeout=5) ''' super(SerialTarget, self).__init__(name, logger, expect_response) self.device = device self.baudrate = baudrate self.timeout = timeout self.open_at = open_at if self.open_at not in ['setup', 'pre_test']: raise KittyException('open_at must be either "setup" or "pre_test"')
def __init__(self, depends_on, encoder=ENC_BITS_DEFAULT, fuzzable=True, name=None): ''' :param depends_on: (name of) field we depend on :type encoder: :class:`~kitty.model.low_level.encoder.BitsEncoder` :param encoder: encoder for the field :param fuzzable: is container fuzzable :param name: (unique) name of the container ''' self._rendered_field = None self.dependency_type = Calculated.VALUE_BASED super(Calculated, self).__init__(value=self.__class__._default_value_, encoder=encoder, fuzzable=fuzzable, name=name) if isinstance(depends_on, str): self._field_name = depends_on self._field = None elif isinstance(depends_on, BaseField): self._field_name = None self._field = depends_on else: raise KittyException( 'depends_on parameter (%s) is neither a string nor a valid field' % depends_on)
def __init__(self, depends_on, length, correction=None, encoder=ENC_INT_DEFAULT, fuzzable=False, name=None): ''' :param depends_on: (name of) field we depend on :param length: length of the FieldIntProperty field (in bits) :type corrention: int or func(int) -> int :param correction: correction function, or value for the index :type encoder: :class:`~kitty.model.low_level.encoder.BitFieldEncoder` :param encoder: encoder for the field (default: ENC_INT_DEFAULT) :param fuzzable: is container fuzzable :param name: (unique) name of the container (default: None) ''' if correction: if not callable(correction): if not isinstance(correction, int): raise KittyException( 'correction must be int, function or None!') self._correction = correction bit_field = BitField(value=0, length=length, encoder=encoder) super(FieldIntProperty, self).__init__(depends_on=depends_on, bit_field=bit_field, calc_func=None, fuzzable=fuzzable, name=name) self.dependency_type = Calculated.FIELD_PROP_BASED
def render(self, ctx=None): ''' :param ctx: rendering context in which the method was called :rtype: `Bits` :return: rendered value of the container ''' self._initialize() render_count = 1 if ctx is None: ctx = RenderContext() if self._need_second_pass: render_count = 2 ctx.push(self) if self.is_default(): self._current_rendered = self._default_rendered else: if self.offset is None: self.offset = 0 for i in range(render_count): offset = self.offset rendered = BitArray() for field in self._fields: field.set_offset(offset) frendered = field.render(ctx) if not isinstance(frendered, Bits): raise KittyException('the field %s:%s was rendered to type %s, you should probably wrap it with appropriate encoder' % ( field.get_name(), type(field), type(frendered))) rendered.append(frendered) offset += len(frendered) self.set_current_value(rendered) ctx.pop() return self._current_rendered
def _handle_options(self, option_line): ''' Handle options from command line, in docopt style. This allows passing arguments to the fuzzer from the command line without the need to re-write it in each runner. :param option_line: string with the command line options to be parsed. ''' if option_line is not None: usage = ''' These are the options to the kitty fuzzer object, not the options to the runner. Usage: fuzzer [options] [-v ...] Options: -d --delay <delay> delay between tests in secodes, float number -f --session <session-file> session file name to use -n --no-env-test don't perform environment test before the fuzzing session -r --retest <session-file> retest failed/error tests from a session file -t --test-list <test-list> a comma delimited test list string of the form "-10,12,15-20,30-" -v --verbose be more verbose in the log Removed options: end, start - use --test-list instead ''' options = docopt.docopt(usage, shlex.split(option_line)) # ranges if options['--retest']: retest_file = options['--retest'] try: test_list_str = self._get_test_list_from_session_file( retest_file) except Exception as ex: raise KittyException( 'Failed to open session file (%s) for retesting: %s' % (retest_file, ex)) else: test_list_str = options['--test-list'] self._set_test_ranges(None, None, test_list_str) # session file session_file = options['--session'] if session_file is not None: self.set_session_file(session_file) # delay between tests delay = options['--delay'] if delay is not None: self.set_delay_between_tests(float(delay)) # environment test skip_env_test = options['--no-env-test'] if skip_env_test: self.set_skip_env_test(True) # verbosity verbosity = options['--verbose'] self.set_verbosity(verbosity)
def __init__(self, field_count, fields=[], delim=None, encoder=ENC_BITS_DEFAULT, fuzzable=True, name=None): ''' :param field_count: how many fields to omit in each mutation :type fields: field or iterable of fields :param fields: enclosed field(s) (default: []) :type delim: field :param delim: delimiter between elements in the list (default: None) :type encoder: BitsEncoder :param encoder: encoder for the container (default: ENC_BITS_DEFAULT) :param fuzzable: is container fuzzable (default: True) :param name: (unique) name of the container (default: None) ''' if field_count < 1: raise KittyException('field_count (%s) < 1' % (field_count)) super(FieldRangeMutator, self).__init__(fields=fields, encoder=encoder, fuzzable=fuzzable, name=name) self._field_count = field_count self._orig_fields = [] self._delim = delim
def encode(self, value, length, signed): ''' :param value: value to encode :param length: length of value in bits :param signed: is value signed ''' if signed: raise KittyException('Signed MultiBytes not supported yet, sorry') # split to septets if value: bytes_arr = [] while value: bytes_arr.append((value & 0x7f) | 0x80) value >>= 7 else: bytes_arr = [0] # reverse if big endian endian if self._mode == 'be': bytes_arr.reverse() # remove msb from last byte bytes_arr[-1] = bytes_arr[-1] & 0x7f multi_bytes = ''.join(chr(x) for x in bytes_arr) return Bits(bytes=multi_bytes)
def copy(self): ''' We might want to change it in the future, but for now... :raises: :class:`~kitty.core.KittyException`, as it should not be copied ''' raise KittyException('Template should NOT be copied')
def pre_test(self, test_num=int) -> None: """ This is only checks whether the target is available or not. :param test_num: The number of the test case. """ super(ProtobufTarget, self).pre_test(test_num) retry_count = 0 while self.socket is None and retry_count < self.max_retries: sock = self._get_socket() if self.timeout is not None: sock.settimeout(self.timeout) try: retry_count += 1 sock.connect((self.host, self.port)) self.socket = sock except Exception: sock.close() self.logger.error( f"PBTARGET - TCP Error: {traceback.format_exc()}") self.logger.error( f"PBTARGET - TCP Failed to connect to target server, retrying..." ) time.sleep(1) if self.socket is None: raise (KittyException( 'PBTARGET - TCPTarget: (pre_test) cannot connect to server (retries = %d' % retry_count))
def __init__(self, value, encoder=ENC_BITS_DEFAULT, fuzzable=True, name=None): ''' :param value: default value :type encoder: :class:`~kitty.model.low_level.encoder.BaseEncoder` :param encoder: encoder for the field :param fuzzable: is field fuzzable (default: True) :param name: name of the object (default: None) ''' if name and '/' in name: raise KittyException('Name (%s) includes invalid chars /' % (name)) super(BaseField, self).__init__(name, logger=logging.getLogger('DataModel')) kassert.is_of_types(encoder, self.__class__._encoder_type_) self._encoder = encoder self._num_mutations = 0 self._fuzzable = fuzzable self._default_value = value self._default_rendered = self._encode_value(self._default_value) self._current_value = value self._current_rendered = self._default_rendered self._current_index = -1 self.enclosing = None self._initialized = False self._hash = None self._need_second_pass = False self.offset = None self._controlled = False
def __init__(self, field_count, fields=[], encoder=ENC_BITS_DEFAULT, fuzzable=True, name=None): ''' :param field_count: how many fields to omit in each mutation :type fields: field or iterable of fields :param fields: enclosed field(s) (default: []) :type encoder: BitsEncoder :param encoder: encoder for the container (default: ENC_BITS_DEFAULT) :param fuzzable: is container fuzzable (default: True) :param name: (unique) name of the container (default: None) :example: :: RotateMutator(field_count=3, fields=[ Static('A'), Static('B'), Static('C'), Static('D'), ]) will result in: BCAD, CABD, ACDB, ADBC ''' if field_count < 2: raise KittyException('field_count (%s) < 2' % (field_count)) super(RotateMutator, self).__init__(field_count=field_count, fields=fields, encoder=encoder, fuzzable=fuzzable, name=name)
def not_none(obj): ''' :param obj: object to assert :raise: an exception if obj is not None ''' if obj is None: raise KittyException('object is None')
def _validate_strategy(self, strategy): valid = False if re.match(Stage._all_pattern, strategy): valid = True elif re.match(Stage._random_pattern, strategy): valid = True elif re.match(Stage._const_pattern, strategy): valid = True elif re.match(Stage._range_pattern, strategy): ts = strategy.split('-') min_sequence = int(ts[0]) max_sequence = int(ts[1]) if max_sequence < min_sequence: raise KittyException('bad range strategy %s, max < min' % strategy) valid = True if not valid: raise KittyException('strategy %s is not valid' % strategy)
def is_in(obj, it): ''' :param obj: object to assert :param it: iterable of elements we assert obj is in :raise: an exception if obj is in an iterable ''' if obj not in it: raise KittyException('(%s) is not in %s' % (obj, it))
def get_field_by_name(self, name): ''' :param name: name of field to get :raises: :class:`~kitty.core.KittyException` if no direct subfield with this name ''' raise KittyException( 'Basic field (%s) does not contain any fields inside (looked for "%s")' % (self, name))
def _check_args(self): ''' This is a massive check. argh... ''' if self.key: if len(self.key) not in self._key_sizes_: raise KittyException('provided key size (%d) not in %s' % (len(self.key), self._key_sizes_)) if self.key_provider: raise KittyException('You should not provide both key and key_provider.') elif self.key_provider: if not callable(self.key_provider): raise KittyException('key_provider must be callable') if not self.key_size: if self._default_key_size_: self.key_size = self._default_key_size_ else: raise KittyException('key_size should be specified when using key_provider') if self.key_size not in self._key_sizes_: raise KittyException('key size (%d) not a valid one (use %s)' % (self.key_size, self._key_sizes_)) else: raise KittyException('You need to provide either key or key_provider') if not self.iv: self.iv = '\x00' * self._iv_size_ if len(self.iv) != self._iv_size_: raise KittyException('Invalid iv size: %#x. Expected: %#x') if not self.padder: self.padder = self._zero_padder if self.mode is None: self.mode = self._default_mode_
def is_of_types(obj, the_types): ''' :param obj: object to assert :param the_types: iterable of types, or a signle type :raise: an exception if obj is not an instance of types ''' if not isinstance(obj, the_types): raise KittyException('object type (%s) is not one of (%s)' % (type(obj), the_types))
def encode(self, value): ''' :param value: value to encode ''' kassert.is_of_types(value, Bits) if len(value) % 8 != 0: raise KittyException( 'this encoder cannot encode bits that are not byte aligned') return self._encoder.encode(value.bytes)