Update examples of the custom mutator

- Merge `examples/python_mutators` into `examples/custom_mutators`
- Remove `examples/python_mutators`
- Update existing examples to demonstrate new APIs
This commit is contained in:
h1994st
2020-03-04 01:09:37 -05:00
parent 42ce48db39
commit 38e7dd2b9e
11 changed files with 313 additions and 156 deletions

View File

@ -1,4 +1,22 @@
# A simple example for AFL_CUSTOM_MUTATOR_LIBRARY
# Examples for the custom mutator
This is a simple example for the AFL_CUSTOM_MUTATOR_LIBRARY feature.
For more information see [docs/custom_mutator.md](../docs/custom_mutator.md)
These are example and helper files for the custom mutator feature.
See [docs/python_mutators.md](../docs/custom_mutators.md) for more information
Note that if you compile with python3.7 you must use python3 scripts, and if
you use pyton2.7 to compile python2 scripts!
example.c - this is a simple example written in C and should be compiled to a
shared library
example.py - this is the template you can use, the functions are there but they
are empty
simple-chunk-replace.py - this is a simple example where chunks are replaced
common.py - this can be used for common functions and helpers.
the examples do not use this though. But you can :)
wrapper_afl_min.py - mutation of XML documents, loads XmlMutatorMin.py
XmlMutatorMin.py - module for XML mutation

View File

@ -0,0 +1,332 @@
#!/usr/bin/python
""" Mutation of XML documents, should be called from one of its wrappers (CLI, AFL, ...) """
from __future__ import print_function
from copy import deepcopy
from lxml import etree as ET
import random, re, io
###########################
# The XmlMutatorMin class #
###########################
class XmlMutatorMin:
"""
Optionals parameters:
seed Seed used by the PRNG (default: "RANDOM")
verbose Verbosity (default: False)
"""
def __init__(self, seed="RANDOM", verbose=False):
""" Initialize seed, database and mutators """
# Verbosity
self.verbose = verbose
# Initialize PRNG
self.seed = str(seed)
if self.seed == "RANDOM":
random.seed()
else:
if self.verbose:
print("Static seed '%s'" % self.seed)
random.seed(self.seed)
# Initialize input and output documents
self.input_tree = None
self.tree = None
# High-level mutators (no database needed)
hl_mutators_delete = ["del_node_and_children", "del_node_but_children", "del_attribute", "del_content"] # Delete items
hl_mutators_fuzz = ["fuzz_attribute"] # Randomly change attribute values
# Exposed mutators
self.hl_mutators_all = hl_mutators_fuzz + hl_mutators_delete
def __parse_xml(self, xml):
""" Parse an XML string. Basic wrapper around lxml.parse() """
try:
# Function parse() takes care of comments / DTD / processing instructions / ...
tree = ET.parse(io.BytesIO(xml))
except ET.ParseError:
raise RuntimeError("XML isn't well-formed!")
except LookupError as e:
raise RuntimeError(e)
# Return a document wrapper
return tree
def __exec_among(self, module, functions, min_times, max_times):
""" Randomly execute $functions between $min and $max times """
for i in xrange(random.randint(min_times, max_times)):
# Function names are mangled because they are "private"
getattr(module, "_XmlMutatorMin__" + random.choice(functions))()
def __serialize_xml(self, tree):
""" Serialize a XML document. Basic wrapper around lxml.tostring() """
return ET.tostring(tree, with_tail=False, xml_declaration=True, encoding=tree.docinfo.encoding)
def __ver(self, version):
""" Helper for displaying lxml version numbers """
return ".".join(map(str, version))
def reset(self):
""" Reset the mutator """
self.tree = deepcopy(self.input_tree)
def init_from_string(self, input_string):
""" Initialize the mutator from a XML string """
# Get a pointer to the top-element
self.input_tree = self.__parse_xml(input_string)
# Get a working copy
self.tree = deepcopy(self.input_tree)
def save_to_string(self):
""" Return the current XML document as UTF-8 string """
# Return a text version of the tree
return self.__serialize_xml(self.tree)
def __pick_element(self, exclude_root_node=False):
""" Pick a random element from the current document """
# Get a list of all elements, but nodes like PI and comments
elems = list(self.tree.getroot().iter(tag=ET.Element))
# Is the root node excluded?
if exclude_root_node:
start = 1
else:
start = 0
# Pick a random element
try:
elem_id = random.randint(start, len(elems) - 1)
elem = elems[elem_id]
except ValueError:
# Should only occurs if "exclude_root_node = True"
return (None, None)
return (elem_id, elem)
def __fuzz_attribute(self):
""" Fuzz (part of) an attribute value """
# Select a node to modify
(rand_elem_id, rand_elem) = self.__pick_element()
# Get all the attributes
attribs = rand_elem.keys()
# Is there attributes?
if len(attribs) < 1:
if self.verbose:
print("No attribute: can't replace!")
return
# Pick a random attribute
rand_attrib_id = random.randint(0, len(attribs) - 1)
rand_attrib = attribs[rand_attrib_id]
# We have the attribute to modify
# Get its value
attrib_value = rand_elem.get(rand_attrib)
# print("- Value: " + attrib_value)
# Should we work on the whole value?
func_call = "(?P<func>[a-zA-Z:\-]+)\((?P<args>.*?)\)"
p = re.compile(func_call)
l = p.findall(attrib_value)
if random.choice((True, False)) and l:
# Randomly pick one the function calls
(func, args) = random.choice(l)
# Split by "," and randomly pick one of the arguments
value = random.choice(args.split(','))
# Remove superfluous characters
unclean_value = value
value = value.strip(" ").strip("'")
# print("Selected argument: [%s]" % value)
else:
value = attrib_value
# For each type, define some possible replacement values
choices_number = ( \
"0", \
"11111", \
"-128", \
"2", \
"-1", \
"1/3", \
"42/0", \
"1094861636 idiv 1.0", \
"-1123329771506872 idiv 3.8", \
"17=$numericRTF", \
str(3 + random.randrange(0, 100)), \
)
choices_letter = ( \
"P" * (25 * random.randrange(1, 100)), \
"%s%s%s%s%s%s", \
"foobar", \
)
choices_alnum = ( \
"Abc123", \
"020F0302020204030204", \
"020F0302020204030204" * (random.randrange(5, 20)), \
)
# Fuzz the value
if random.choice((True,False)) and value == "":
# Empty
new_value = value
elif random.choice((True,False)) and value.isdigit():
# Numbers
new_value = random.choice(choices_number)
elif random.choice((True,False)) and value.isalpha():
# Letters
new_value = random.choice(choices_letter)
elif random.choice((True,False)) and value.isalnum():
# Alphanumeric
new_value = random.choice(choices_alnum)
else:
# Default type
new_value = random.choice(choices_alnum + choices_letter + choices_number)
# If we worked on a substring, apply changes to the whole string
if value != attrib_value:
# No ' around empty values
if new_value != "" and value != "":
new_value = "'" + new_value + "'"
# Apply changes
new_value = attrib_value.replace(unclean_value, new_value)
# Log something
if self.verbose:
print("Fuzzing attribute #%i '%s' of tag #%i '%s'" % (rand_attrib_id, rand_attrib, rand_elem_id, rand_elem.tag))
# Modify the attribute
rand_elem.set(rand_attrib, new_value.decode("utf-8"))
def __del_node_and_children(self):
""" High-level minimizing mutator
Delete a random node and its children (i.e. delete a random tree) """
self.__del_node(True)
def __del_node_but_children(self):
""" High-level minimizing mutator
Delete a random node but its children (i.e. link them to the parent of the deleted node) """
self.__del_node(False)
def __del_node(self, delete_children):
""" Called by the __del_node_* mutators """
# Select a node to modify (but the root one)
(rand_elem_id, rand_elem) = self.__pick_element(exclude_root_node=True)
# If the document includes only a top-level element
# Then we can't pick a element (given that "exclude_root_node = True")
# Is the document deep enough?
if rand_elem is None:
if self.verbose:
print("Can't delete a node: document not deep enough!")
return
# Log something
if self.verbose:
but_or_and = "and" if delete_children else "but"
print("Deleting tag #%i '%s' %s its children" % (rand_elem_id, rand_elem.tag, but_or_and))
if delete_children is False:
# Link children of the random (soon to be deleted) node to its parent
for child in rand_elem:
rand_elem.getparent().append(child)
# Remove the node
rand_elem.getparent().remove(rand_elem)
def __del_content(self):
""" High-level minimizing mutator
Delete the attributes and children of a random node """
# Select a node to modify
(rand_elem_id, rand_elem) = self.__pick_element()
# Log something
if self.verbose:
print("Reseting tag #%i '%s'" % (rand_elem_id, rand_elem.tag))
# Reset the node
rand_elem.clear()
def __del_attribute(self):
""" High-level minimizing mutator
Delete a random attribute from a random node """
# Select a node to modify
(rand_elem_id, rand_elem) = self.__pick_element()
# Get all the attributes
attribs = rand_elem.keys()
# Is there attributes?
if len(attribs) < 1:
if self.verbose:
print("No attribute: can't delete!")
return
# Pick a random attribute
rand_attrib_id = random.randint(0, len(attribs) - 1)
rand_attrib = attribs[rand_attrib_id]
# Log something
if self.verbose:
print("Deleting attribute #%i '%s' of tag #%i '%s'" % (rand_attrib_id, rand_attrib, rand_elem_id, rand_elem.tag))
# Delete the attribute
rand_elem.attrib.pop(rand_attrib)
def mutate(self, min=1, max=5):
""" Execute some high-level mutators between $min and $max times, then some medium-level ones """
# High-level mutation
self.__exec_among(self, self.hl_mutators_all, min, max)

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
# encoding: utf-8
'''
Module containing functions shared between multiple AFL modules
@author: Christian Holler (:decoder)
@license:
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
@contact: choller@mozilla.com
'''
from __future__ import print_function
import random
import os
import re
def randel(l):
if not l:
return None
return l[random.randint(0, len(l)-1)]
def randel_pop(l):
if not l:
return None
return l.pop(random.randint(0, len(l)-1))
def write_exc_example(data, exc):
exc_name = re.sub(r'[^a-zA-Z0-9]', '_', repr(exc))
if not os.path.exists(exc_name):
with open(exc_name, 'w') as f:
f.write(data)

View File

@ -0,0 +1,177 @@
/*
New Custom Mutator for AFL++
Written by Khaled Yakdan <yakdan@code-intelligence.de>
Andrea Fioraldi <andreafioraldi@gmail.com>
Shengtuo Hu <h1994st@gmail.com>
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static const char *commands[] = {
"GET",
"PUT",
"DEL",
};
static size_t data_size = 100;
void afl_custom_init(unsigned int seed) {
srand(seed);
}
/**
* Perform custom mutations on a given input
*
* (Optional for now. Required in the future)
*
* @param[in] buf Input data to be mutated
* @param[in] buf_size Size of input data
* @param[in] add_buf Buffer containing the additional test case
* @param[in] add_buf_size Size of the additional test case
* @param[out] mutated_out Buffer to store the mutated input
* @param[in] max_size Maximum size of the mutated output. The mutation must not
* produce data larger than max_size.
* @return Size of the mutated output.
*/
size_t afl_custom_fuzz(uint8_t *buf, size_t buf_size,
uint8_t *add_buf,size_t add_buf_size, // add_buf can be NULL
uint8_t *mutated_out, size_t max_size) {
// Make sure that the packet size does not exceed the maximum size expected by
// the fuzzer
size_t mutated_size = data_size <= max_size ? data_size : max_size;
// Randomly select a command string to add as a header to the packet
memcpy(mutated_out, commands[rand() % 3], 3);
// Mutate the payload of the packet
for (int i = 3; i < mutated_size; i++) {
mutated_out[i] = (data[i] + rand() % 10) & 0xff;
}
return mutated_size;
}
/**
* A post-processing function to use right before AFL writes the test case to
* disk in order to execute the target.
*
* (Optional) If this functionality is not needed, simply don't define this
* function.
*
* @param[in] buf Buffer containing the test case to be executed
* @param[in] buf_size Size of the test case
* @param[out] out_buf Pointer to the buffer containing the test case after
* processing. External library should allocate memory for out_buf. AFL++
* will release the memory after saving the test case.
* @return Size of the output buffer after processing
*/
size_t afl_custom_pre_save(uint8_t *buf, size_t buf_size, uint8_t **out_buf) {
size_t out_buf_size;
out_buf_size = buf_size;
// External mutator should allocate memory for `out_buf`
*out_buf = malloc(out_buf_size);
memcpy(*out_buf, buf, out_buf_size);
return out_buf_size;
}
uint8_t *trim_buf;
size_t trim_buf_size
int trimmming_steps;
int cur_step;
/**
* This method is called at the start of each trimming operation and receives
* the initial buffer. It should return the amount of iteration steps possible
* on this input (e.g. if your input has n elements and you want to remove
* them one by one, return n, if you do a binary search, return log(n),
* and so on...).
*
* If your trimming algorithm doesn't allow you to determine the amount of
* (remaining) steps easily (esp. while running), then you can alternatively
* return 1 here and always return 0 in post_trim until you are finished and
* no steps remain. In that case, returning 1 in post_trim will end the
* trimming routine. The whole current index/max iterations stuff is only used
* to show progress.
*
* (Optional)
*
* @param buf Buffer containing the test case
* @param buf_size Size of the test case
* @return The amount of possible iteration steps to trim the input
*/
int afl_custom_init_trim(uint8_t *buf, size_t buf_size) {
// We simply trim once
trimmming_steps = 1;
cur_step = 0;
trim_buf = buf;
trim_buf_size = buf_size;
return trimmming_steps;
}
/**
* This method is called for each trimming operation. It doesn't have any
* arguments because we already have the initial buffer from init_trim and we
* can memorize the current state in global variables. This can also save
* reparsing steps for each iteration. It should return the trimmed input
* buffer, where the returned data must not exceed the initial input data in
* length. Returning anything that is larger than the original data (passed
* to init_trim) will result in a fatal abort of AFLFuzz.
*
* (Optional)
*
* @param[out] out_buf Pointer to the buffer containing the trimmed test case.
* External library should allocate memory for out_buf. AFL++ will release
* the memory after saving the test case.
* @param[out] out_buf_size Pointer to the size of the trimmed test case
*/
void afl_custom_trim(uint8_t **out_buf, size_t* out_buf_size) {
*out_buf_size = trim_buf_size - 1;
// External mutator should allocate memory for `out_buf`
*out_buf = malloc(*out_buf_size);
// Remove the last byte of the trimming input
memcpy(*out_buf, trim_buf, *out_buf_size);
}
/**
* This method is called after each trim operation to inform you if your
* trimming step was successful or not (in terms of coverage). If you receive
* a failure here, you should reset your input to the last known good state.
*
* (Optional)
*
* @param success Indicates if the last trim operation was successful.
* @return The next trim iteration index (from 0 to the maximum amount of
* steps returned in init_trim)
*/
int afl_custom_post_trim(int success) {
if (success) {
++cur_step;
return cur_step;
}
return trimmming_steps;
}

View File

@ -0,0 +1,122 @@
#!/usr/bin/env python
# encoding: utf-8
'''
Example Python Module for AFLFuzz
@author: Christian Holler (:decoder)
@license:
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
@contact: choller@mozilla.com
'''
import random
def init(seed):
'''
Called once when AFLFuzz starts up. Used to seed our RNG.
@type seed: int
@param seed: A 32-bit random value
'''
random.seed(seed)
def fuzz(buf, add_buf, max_size):
'''
Called per fuzzing iteration.
@type buf: bytearray
@param buf: The buffer that should be mutated.
@type add_buf: bytearray
@param add_buf: A second buffer that can be used as mutation source.
@type max_size: int
@param max_size: Maximum size of the mutated output. The mutation must not
produce data larger than max_size.
@rtype: bytearray
@return: A new bytearray containing the mutated data
'''
ret = bytearray(buf)
# Do something interesting with ret
return ret
# Uncomment and implement the following methods if you want to use a custom
# trimming algorithm. See also the documentation for a better API description.
# def init_trim(buf):
# '''
# Called per trimming iteration.
#
# @type buf: bytearray
# @param buf: The buffer that should be trimmed.
#
# @rtype: int
# @return: The maximum number of trimming steps.
# '''
# global ...
#
# # Initialize global variables
#
# # Figure out how many trimming steps are possible.
# # If this is not possible for your trimming, you can
# # return 1 instead and always return 0 in post_trim
# # until you are done (then you return 1).
#
# return steps
#
# def trim():
# '''
# Called per trimming iteration.
#
# @rtype: bytearray
# @return: A new bytearray containing the trimmed data.
# '''
# global ...
#
# # Implement the actual trimming here
#
# return bytearray(...)
#
# def post_trim(success):
# '''
# Called after each trimming operation.
#
# @type success: bool
# @param success: Indicates if the last trim operation was successful.
#
# @rtype: int
# @return: The next trim index (0 to max number of steps) where max
# number of steps indicates the trimming is done.
# '''
# global ...
#
# if not success:
# # Restore last known successful input, determine next index
# else:
# # Just determine the next index, based on what was successfully
# # removed in the last step
#
# return next_index
#
# def pre_save(buf):
# '''
# Called just before the execution to write the test case in the format
# expected by the target
#
# @type buf: bytearray
# @param buf: The buffer containing the test case to be executed
#
# @rtype: bytearray
# @return: The buffer containing the test case after
# '''
# return buf
#

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
# encoding: utf-8
'''
Simple Chunk Cross-Over Replacement Module for AFLFuzz
@author: Christian Holler (:decoder)
@license:
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
@contact: choller@mozilla.com
'''
import random
def init(seed):
'''
Called once when AFLFuzz starts up. Used to seed our RNG.
@type seed: int
@param seed: A 32-bit random value
'''
# Seed our RNG
random.seed(seed)
def fuzz(buf, add_buf, max_size):
'''
Called per fuzzing iteration.
@type buf: bytearray
@param buf: The buffer that should be mutated.
@type add_buf: bytearray
@param add_buf: A second buffer that can be used as mutation source.
@type max_size: int
@param max_size: Maximum size of the mutated output. The mutation must not
produce data larger than max_size.
@rtype: bytearray
@return: A new bytearray containing the mutated data
'''
# Make a copy of our input buffer for returning
ret = bytearray(buf)
# Take a random fragment length between 2 and 32 (or less if add_buf is shorter)
fragment_len = random.randint(1, min(len(add_buf), 32))
# Determine a random source index where to take the data chunk from
rand_src_idx = random.randint(0, len(add_buf) - fragment_len)
# Determine a random destination index where to put the data chunk
rand_dst_idx = random.randint(0, len(buf))
# Make the chunk replacement
ret[rand_dst_idx:rand_dst_idx + fragment_len] = add_buf[rand_src_idx:rand_src_idx + fragment_len]
# Return data
return ret

View File

@ -1,49 +0,0 @@
/*
Simple Custom Mutator for AFL
Written by Khaled Yakdan <yakdan@code-intelligence.de>
This a simple mutator that assumes that the generates messages starting with
one of the three strings GET, PUT, or DEL followed by a payload. The mutator
randomly selects a commend and mutates the payload of the seed provided as
input.
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static const char *commands[] = {
"GET",
"PUT",
"DEL",
};
static size_t data_size = 100;
size_t afl_custom_mutator(uint8_t *data, size_t size, uint8_t *mutated_out,
size_t max_size, unsigned int seed) {
// Seed the PRNG
srand(seed);
// Make sure that the packet size does not exceed the maximum size expected by
// the fuzzer
size_t mutated_size = data_size <= max_size ? data_size : max_size;
// Randomly select a command string to add as a header to the packet
memcpy(mutated_out, commands[rand() % 3], 3);
// Mutate the payload of the packet
for (int i = 3; i < mutated_size; i++) {
mutated_out[i] = (data[i] + rand() % 10) & 0xff;
}
return mutated_size;
}

View File

@ -0,0 +1,118 @@
#!/usr/bin/env python
from XmlMutatorMin import XmlMutatorMin
# Default settings (production mode)
__mutator__ = None
__seed__ = "RANDOM"
__log__ = False
__log_file__ = "wrapper.log"
# AFL functions
def log(text):
"""
Logger
"""
global __seed__
global __log__
global __log_file__
if __log__:
with open(__log_file__, "a") as logf:
logf.write("[%s] %s\n" % (__seed__, text))
def init(seed):
"""
Called once when AFL starts up. Seed is used to identify the AFL instance in log files
"""
global __mutator__
global __seed__
# Get the seed
__seed__ = seed
# Create a global mutation class
try:
__mutator__ = XmlMutatorMin(__seed__, verbose=__log__)
log("init(): Mutator created")
except RuntimeError as e:
log("init(): Can't create mutator: %s" % e.message)
def fuzz(buf, add_buf, max_size):
"""
Called for each fuzzing iteration.
"""
global __mutator__
# Do we have a working mutator object?
if __mutator__ is None:
log("fuzz(): Can't fuzz, no mutator available")
return buf
# Try to use the AFL buffer
via_buffer = True
# Interpret the AFL buffer (an array of bytes) as a string
if via_buffer:
try:
buf_str = str(buf)
log("fuzz(): AFL buffer converted to a string")
except Exception:
via_buffer = False
log("fuzz(): Can't convert AFL buffer to a string")
# Load XML from the AFL string
if via_buffer:
try:
__mutator__.init_from_string(buf_str)
log("fuzz(): Mutator successfully initialized with AFL buffer (%d bytes)" % len(buf_str))
except Exception:
via_buffer = False
log("fuzz(): Can't initialize mutator with AFL buffer")
# If init from AFL buffer wasn't succesful
if not via_buffer:
log("fuzz(): Returning unmodified AFL buffer")
return buf
# Sucessful initialization -> mutate
try:
__mutator__.mutate(max=5)
log("fuzz(): Input mutated")
except Exception:
log("fuzz(): Can't mutate input => returning buf")
return buf
# Convert mutated data to a array of bytes
try:
data = bytearray(__mutator__.save_to_string())
log("fuzz(): Mutated data converted as bytes")
except Exception:
log("fuzz(): Can't convert mutated data to bytes => returning buf")
return buf
# Everything went fine, returning mutated content
log("fuzz(): Returning %d bytes" % len(data))
return data
# Main (for debug)
if __name__ == '__main__':
__log__ = True
__log_file__ = "/dev/stdout"
__seed__ = "RANDOM"
init(__seed__)
in_1 = bytearray("<foo ddd='eeee'>ffff<a b='c' d='456' eee='ffffff'>zzzzzzzzzzzz</a><b yyy='YYY' zzz='ZZZ'></b></foo>")
in_2 = bytearray("<abc abc123='456' abcCBA='ppppppppppppppppppppppppppppp'/>")
out = fuzz(in_1, in_2)
print(out)