about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore18
-rwxr-xr-xafl-system-config1
-rw-r--r--afl-tmin.c189
-rw-r--r--docs/ChangeLog8
-rw-r--r--docs/PATCHES5
-rw-r--r--docs/README4
-rw-r--r--docs/env_variables.txt6
-rw-r--r--llvm_mode/Makefile2
-rw-r--r--llvm_mode/README.laf-intel8
-rw-r--r--llvm_mode/afl-clang-fast.c6
-rw-r--r--llvm_mode/split-compares-pass.so.cc2
-rw-r--r--python_mutators/README4
-rw-r--r--python_mutators/XmlMutatorMin.py331
-rw-r--r--python_mutators/wrapper_afl_min.py117
14 files changed, 667 insertions, 34 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..cc0fd5c7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+.gitignore
+TODO
+afl-analyze
+afl-as
+afl-clang
+afl-clang++
+afl-clang-fast
+afl-clang-fast++
+afl-fuzz
+afl-g++
+afl-gcc
+afl-gotcpu
+afl-qemu-trace
+afl-showmap
+afl-tmin
+as
+qemu_mode/qemu-3.1.0
+qemu_mode/qemu-3.1.0.tar.xz
diff --git a/afl-system-config b/afl-system-config
index 88564c20..7538bc29 100755
--- a/afl-system-config
+++ b/afl-system-config
@@ -8,6 +8,7 @@ sysctl -w kernel.sched_migration_cost_ns=50000000
 sysctl -w kernel.sched_latency_ns=250000000
 echo never > /sys/kernel/mm/transparent_hugepage/enabled
 echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null
+test -e /sys/devices/system/cpu/intel_pstate/no_turbo && echo 0 > /sys/devices/system/cpu/intel_pstate/no_turbo
 echo
 echo It is recommended to boot the kernel with lots of security off - if you are running a machine that is in a secured network - so set this:
 echo '/etc/default/grub:GRUB_CMDLINE_LINUX_DEFAULT="ibpb=off ibrs=off kpti=off l1tf=off mds=off mitigations=off no_stf_barrier noibpb noibrs nopcid nopti nospec_store_bypass_disable nospectre_v1 nospectre_v2 pcid=off pti=off spec_store_bypass_disable=off spectre_v2=off stf_barrier=off"'
diff --git a/afl-tmin.c b/afl-tmin.c
index 2d839041..a42be6e9 100644
--- a/afl-tmin.c
+++ b/afl-tmin.c
@@ -44,7 +44,11 @@
 #include <sys/types.h>
 #include <sys/resource.h>
 
-static s32 child_pid;                 /* PID of the tested program         */
+static s32 forksrv_pid,               /* PID of the fork server           */
+           child_pid;                 /* PID of the tested program        */
+
+static s32 fsrv_ctl_fd,               /* Fork server control pipe (write) */
+           fsrv_st_fd;                /* Fork server status pipe (read)   */
 
 static u8 *trace_bits,                /* SHM with instrumentation bitmap   */
           *mask_bitmap;               /* Mask for trace bits (-B)          */
@@ -55,6 +59,8 @@ static u8 *in_file,                   /* Minimizer input test case         */
           *target_path,               /* Path to target binary             */
           *doc_path;                  /* Path to docs                      */
 
+static s32 prog_in_fd;                /* Persistent fd for prog_in         */
+
 static u8* in_data;                   /* Input data for trimming           */
 
 static u32 in_len,                    /* Input data length                 */
@@ -236,38 +242,70 @@ static s32 write_to_file(u8* path, u8* mem, u32 len) {
 
 }
 
+/* Write modified data to file for testing. If use_stdin is clear, the old file
+   is unlinked and a new one is created. Otherwise, prog_in_fd is rewound and
+   truncated. */
+
+static void write_to_testcase(void* mem, u32 len) {
+
+  s32 fd = prog_in_fd;
+
+  if (!use_stdin) {
+
+    unlink(prog_in); /* Ignore errors. */
+
+    fd = open(prog_in, O_WRONLY | O_CREAT | O_EXCL, 0600);
+
+    if (fd < 0) PFATAL("Unable to create '%s'", prog_in);
+
+  } else lseek(fd, 0, SEEK_SET);
+
+  ck_write(fd, mem, len, prog_in);
+
+  if (use_stdin) {
+
+    if (ftruncate(fd, len)) PFATAL("ftruncate() failed");
+    lseek(fd, 0, SEEK_SET);
+
+  } else close(fd);
+
+}
+
+
 
 /* Handle timeout signal. */
 
 static void handle_timeout(int sig) {
 
+  if (child_pid > 0) {
+
   child_timed_out = 1;
-  if (child_pid > 0) kill(child_pid, SIGKILL);
+    kill(child_pid, SIGKILL);
 
-}
+  } else if (child_pid == -1 && forksrv_pid > 0) {
 
+    child_timed_out = 1;
+    kill(forksrv_pid, SIGKILL);
 
-/* Execute target application. Returns 0 if the changes are a dud, or
-   1 if they should be kept. */
+  }
 
-static u8 run_target(char** argv, u8* mem, u32 len, u8 first_run) {
+}
 
+/* start the app and it's forkserver */
+static void init_forkserver(char **argv) {
   static struct itimerval it;
+  int st_pipe[2], ctl_pipe[2];
   int status = 0;
+  s32 rlen;
 
-  s32 prog_in_fd;
-  u32 cksum;
-
-  memset(trace_bits, 0, MAP_SIZE);
-  MEM_BARRIER();
-
-  prog_in_fd = write_to_file(prog_in, mem, len);
+  ACTF("Spinning up the fork server...");
+  if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");
 
-  child_pid = fork();
+  forksrv_pid = fork();
 
-  if (child_pid < 0) PFATAL("fork() failed");
+  if (forksrv_pid < 0) PFATAL("fork() failed");
 
-  if (!child_pid) {
+  if (!forksrv_pid) {
 
     struct rlimit r;
 
@@ -304,6 +342,16 @@ static u8 run_target(char** argv, u8* mem, u32 len, u8 first_run) {
     r.rlim_max = r.rlim_cur = 0;
     setrlimit(RLIMIT_CORE, &r); /* Ignore errors */
 
+    /* Set up control and status pipes, close the unneeded original fds. */
+
+    if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed");
+    if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed");
+
+    close(ctl_pipe[0]);
+    close(ctl_pipe[1]);
+    close(st_pipe[0]);
+    close(st_pipe[1]);
+
     execv(target_path, argv);
 
     *(u32*)trace_bits = EXEC_FAIL_SIG;
@@ -311,17 +359,113 @@ static u8 run_target(char** argv, u8* mem, u32 len, u8 first_run) {
 
   }
 
-  close(prog_in_fd);
+  /* Close the unneeded endpoints. */
+
+  close(ctl_pipe[0]);
+  close(st_pipe[1]);
+
+  fsrv_ctl_fd = ctl_pipe[1];
+  fsrv_st_fd  = st_pipe[0];
+
+  /* Configure timeout, wait for child, cancel timeout. */
+
+  if (exec_tmout) {
+
+    child_timed_out = 0;
+    it.it_value.tv_sec = (exec_tmout * FORK_WAIT_MULT / 1000);
+    it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000;
+
+  }
+
+  setitimer(ITIMER_REAL, &it, NULL);
+
+  rlen = read(fsrv_st_fd, &status, 4);
+
+  it.it_value.tv_sec = 0;
+  it.it_value.tv_usec = 0;
+  setitimer(ITIMER_REAL, &it, NULL);
+
+  /* If we have a four-byte "hello" message from the server, we're all set.
+     Otherwise, try to figure out what went wrong. */
+
+  if (rlen == 4) {
+    ACTF("All right - fork server is up.");
+    return;
+  }
+
+  if (waitpid(forksrv_pid, &status, 0) <= 0)
+    PFATAL("waitpid() failed");
+
+  u8 child_crashed;
+
+  if (WIFSIGNALED(status))
+    child_crashed = 1;
+
+  if (child_timed_out)
+    SAYF(cLRD "\n+++ Program timed off +++\n" cRST);
+  else if (stop_soon)
+    SAYF(cLRD "\n+++ Program aborted by user +++\n" cRST);
+  else if (child_crashed)
+    SAYF(cLRD "\n+++ Program killed by signal %u +++\n" cRST, WTERMSIG(status));
+
+}
+
+
+/* Execute target application. Returns 0 if the changes are a dud, or
+   1 if they should be kept. */
+
+static u8 run_target(char** argv, u8* mem, u32 len, u8 first_run) {
+
+  static struct itimerval it;
+  static u32 prev_timed_out = 0;
+  int status = 0;
+
+  u32 cksum;
+
+  memset(trace_bits, 0, MAP_SIZE);
+  MEM_BARRIER();
+
+  write_to_testcase(mem, len);
+
+  s32 res;
+
+  /* we have the fork server up and running, so simply
+     tell it to have at it, and then read back PID. */
+
+  if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) {
+
+    if (stop_soon) return 0;
+    RPFATAL(res, "Unable to request new process from fork server (OOM?)");
+
+  }
+
+  if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4) {
+
+    if (stop_soon) return 0;
+    RPFATAL(res, "Unable to request new process from fork server (OOM?)");
+
+  }
+
+  if (child_pid <= 0) FATAL("Fork server is misbehaving (OOM?)");
 
   /* Configure timeout, wait for child, cancel timeout. */
 
+  if (exec_tmout) {
+
   child_timed_out = 0;
   it.it_value.tv_sec = (exec_tmout / 1000);
   it.it_value.tv_usec = (exec_tmout % 1000) * 1000;
 
+  }
+
   setitimer(ITIMER_REAL, &it, NULL);
 
-  if (waitpid(child_pid, &status, 0) <= 0) FATAL("waitpid() failed");
+  if ((res = read(fsrv_st_fd, &status, 4)) != 4) {
+
+    if (stop_soon) return 0;
+    RPFATAL(res, "Unable to communicate with fork server (OOM?)");
+
+  }
 
   child_pid = 0;
   it.it_value.tv_sec = 0;
@@ -687,6 +831,13 @@ static void set_up_environment(void) {
 
   }
 
+  unlink(prog_in);
+
+  prog_in_fd = open(prog_in, O_RDWR | O_CREAT | O_EXCL, 0600);
+
+  if (prog_in_fd < 0) PFATAL("Unable to create '%s'", prog_in);
+
+
   /* Set sane defaults... */
 
   x = getenv("ASAN_OPTIONS");
@@ -1113,6 +1264,8 @@ int main(int argc, char** argv) {
 
   read_initial_file();
 
+  init_forkserver(use_argv);
+
   ACTF("Performing dry run (mem limit = %llu MB, timeout = %u ms%s)...",
        mem_limit, exec_tmout, edges_only ? ", edges only" : "");
 
diff --git a/docs/ChangeLog b/docs/ChangeLog
index a533de05..b8d0d7ac 100644
--- a/docs/ChangeLog
+++ b/docs/ChangeLog
@@ -19,15 +19,21 @@ Version ++2.52d (tbd):
 
   - added never zero counters for afl-gcc and optional (because of an
     optimization issue in llvm < 9) for llvm_mode (AFL_LLVM_NEVER_ZERO=1)
+  - more cpu power for afl-system-config
+  - added forkserver patch to afl-tmin, makes it much faster (originally from
+    github.com/nccgroup/TriforceAFL)
   - added whitelist support for llvm_mode via AFL_LLVM_WHITELIST to allow
     only to instrument what is actually interesting. Gives more speed and less
     map pollution (originally by choller@mozilla)
   - added Python Module mutator support, python2.7-dev is autodetected.
     see docs/python_mutators.txt (originally by choller@mozilla)
-  - added AFL_CAL_FAST for slow applications and AFL_DEBUG_CHILD_OUTPUT for debugging
+  - added AFL_CAL_FAST for slow applications and AFL_DEBUG_CHILD_OUTPUT for
+    debugging
   - added a  -s seed  switch to allow afl run with a fixed initial
     seed that is not updated. this is good for performance and path discovery
     tests as the random numbers are deterministic then
+  - llvm_mode LAF_... env variables can now be specified as AFL_LLVM_LAF_...
+    that is longer but in line with other llvm specific env vars
   - ... your idea or patch?
 
 
diff --git a/docs/PATCHES b/docs/PATCHES
index c933e031..cb050218 100644
--- a/docs/PATCHES
+++ b/docs/PATCHES
@@ -19,8 +19,9 @@ afl-qemu-optimize-map.diff		by mh(at)mh-sec(dot)de
 
 + AFLfast additions (github.com/mboehme/aflfast) were incorporated.
 + Qemu 3.1 upgrade with enhancement patches (github.com/andreafioraldi/afl)
-+ Python mutator modules support (github.com/choeller/afl)
-+ Whitelisting in LLVM mode (github.com/choeller/afl)
++ Python mutator modules support (github.com/choller/afl)
++ Whitelisting in LLVM mode (github.com/choller/afl)
++ forkserver patch for afl-tmin (github.com/nccgroup/TriforceAFL)
 
 
 NOT INSTALLED
diff --git a/docs/README b/docs/README
index ca8533f7..f9734528 100644
--- a/docs/README
+++ b/docs/README
@@ -21,8 +21,8 @@ american fuzzy lop plus plus
   https://github.com/andreafioraldi/afl and got the community patches applied
   to it.
 
-  C. Hoellers afl-fuzz Python mutator module and llvm_mode whitelist support
-  was added too (https://github.com/choeller/afl)
+  C. Hollers afl-fuzz Python mutator module and llvm_mode whitelist support
+  was added too (https://github.com/choller/afl)
 
   So all in all this is the best-of AFL that is currently out there :-)
 
diff --git a/docs/env_variables.txt b/docs/env_variables.txt
index f8c6c86a..d854ea8d 100644
--- a/docs/env_variables.txt
+++ b/docs/env_variables.txt
@@ -89,11 +89,11 @@ Then there are a few specific features that are only available in llvm_mode:
     This great feature will split compares to series of single byte comparisons
     to allow afl-fuzz to find otherwise rather impossible paths.
 
-    - Setting LAF_SPLIT_SWITCHES will split switch()es
+    - Setting AFL_LLVM_LAF_SPLIT_SWITCHES will split switch()es
 
-    - Setting LAF_TRANSFORM_COMPARES will split string compare functions
+    - Setting AFL_LLVM_LAF_TRANSFORM_COMPARES will split string compare functions
 
-    - Setting LAF_SPLIT_COMPARES will split > 8 bit CMP instructions
+    - Setting AFL_LLVM_LAF_SPLIT_COMPARES will split > 8 bit CMP instructions
 
     See llvm_mode/README.laf-intel for more information. 
 
diff --git a/llvm_mode/Makefile b/llvm_mode/Makefile
index 76de10c0..b6ab0c61 100644
--- a/llvm_mode/Makefile
+++ b/llvm_mode/Makefile
@@ -116,7 +116,7 @@ endif
 
 test_build: $(PROGS)
 	@echo "[*] Testing the CC wrapper and instrumentation output..."
-	unset AFL_USE_ASAN AFL_USE_MSAN AFL_INST_RATIO; AFL_QUIET=1 AFL_PATH=. AFL_CC=$(CC) LAF_SPLIT_SWITCHES=1 LAF_TRANSFORM_COMPARES=1 LAF_SPLIT_COMPARES=1 ../afl-clang-fast $(CFLAGS) ../test-instr.c -o test-instr $(LDFLAGS)
+	unset AFL_USE_ASAN AFL_USE_MSAN AFL_INST_RATIO; AFL_QUIET=1 AFL_PATH=. AFL_CC=$(CC) AFL_LLVM_LAF_SPLIT_SWITCHES=1 AFL_LLVM_LAF_TRANSFORM_COMPARES=1 AFL_LLVM_LAF_SPLIT_COMPARES=1 ../afl-clang-fast $(CFLAGS) ../test-instr.c -o test-instr $(LDFLAGS)
 	echo 0 | ../afl-showmap -m none -q -o .test-instr0 ./test-instr
 	echo 1 | ../afl-showmap -m none -q -o .test-instr1 ./test-instr
 	@rm -f test-instr
diff --git a/llvm_mode/README.laf-intel b/llvm_mode/README.laf-intel
index 891ab5fd..340216c3 100644
--- a/llvm_mode/README.laf-intel
+++ b/llvm_mode/README.laf-intel
@@ -8,13 +8,13 @@ compile the target project.
 
 The following options exist:
 
-export LAF_SPLIT_SWITCHES=1     Enables the split-switches pass.
+export AFL_LLVM_LAF_SPLIT_SWITCHES=1     Enables the split-switches pass.
 
-export LAF_TRANSFORM_COMPARES=1 Enables the transform-compares pass
+export AFL_LLVM_LAF_TRANSFORM_COMPARES=1 Enables the transform-compares pass
                              (strcmp, memcmp, strncmp, strcasecmp, strncasecmp).
 
-export LAF_SPLIT_COMPARES=1     Enables the split-compares pass.
+export AFL_LLVM_LAF_SPLIT_COMPARES=1     Enables the split-compares pass.
       By default it will split all compares with a bit width <= 64 bits.
       You can change this behaviour by setting 
-          export LAF_SPLIT_COMPARES_BITW=<bit_width>.
+          export AFL_LLVM_LAF_SPLIT_COMPARES_BITW=<bit_width>.
 
diff --git a/llvm_mode/afl-clang-fast.c b/llvm_mode/afl-clang-fast.c
index 1e2e04ea..5bc4ae8c 100644
--- a/llvm_mode/afl-clang-fast.c
+++ b/llvm_mode/afl-clang-fast.c
@@ -120,21 +120,21 @@ static void edit_params(u32 argc, char** argv) {
      http://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards */
 
   // laf
-  if (getenv("LAF_SPLIT_SWITCHES")) {
+  if (getenv("LAF_SPLIT_SWITCHES")||getenv("AFL_LLVM_LAF_SPLIT_SWITCHES")) {
     cc_params[cc_par_cnt++] = "-Xclang";
     cc_params[cc_par_cnt++] = "-load";
     cc_params[cc_par_cnt++] = "-Xclang";
     cc_params[cc_par_cnt++] = alloc_printf("%s/split-switches-pass.so", obj_path);
   }
 
-  if (getenv("LAF_TRANSFORM_COMPARES")) {
+  if (getenv("LAF_TRANSFORM_COMPARES")||getenv("AFL_LLVM_LAF_TRANSFORM_COMPARES")) {
     cc_params[cc_par_cnt++] = "-Xclang";
     cc_params[cc_par_cnt++] = "-load";
     cc_params[cc_par_cnt++] = "-Xclang";
     cc_params[cc_par_cnt++] = alloc_printf("%s/compare-transform-pass.so", obj_path);
   }
 
-  if (getenv("LAF_SPLIT_COMPARES")) {
+  if (getenv("LAF_SPLIT_COMPARES")||getenv("AFL_LLVM_LAF_SPLIT_COMPARES")) {
     cc_params[cc_par_cnt++] = "-Xclang";
     cc_params[cc_par_cnt++] = "-load";
     cc_params[cc_par_cnt++] = "-Xclang";
diff --git a/llvm_mode/split-compares-pass.so.cc b/llvm_mode/split-compares-pass.so.cc
index 25ccb3b4..2ea73aaa 100644
--- a/llvm_mode/split-compares-pass.so.cc
+++ b/llvm_mode/split-compares-pass.so.cc
@@ -477,6 +477,8 @@ bool SplitComparesTransform::runOnModule(Module &M) {
   int bitw = 64;
 
   char* bitw_env = getenv("LAF_SPLIT_COMPARES_BITW");
+  if (!bitw_env)
+    bitw_env = getenv("AFL_LLVM_LAF_SPLIT_COMPARES_BITW");
   if (bitw_env) {
     bitw = atoi(bitw_env);
   }
diff --git a/python_mutators/README b/python_mutators/README
index 174c8a64..21a16e52 100644
--- a/python_mutators/README
+++ b/python_mutators/README
@@ -9,3 +9,7 @@ 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
diff --git a/python_mutators/XmlMutatorMin.py b/python_mutators/XmlMutatorMin.py
new file mode 100644
index 00000000..058b7e61
--- /dev/null
+++ b/python_mutators/XmlMutatorMin.py
@@ -0,0 +1,331 @@
+#!/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)
+
diff --git a/python_mutators/wrapper_afl_min.py b/python_mutators/wrapper_afl_min.py
new file mode 100644
index 00000000..df09b40a
--- /dev/null
+++ b/python_mutators/wrapper_afl_min.py
@@ -0,0 +1,117 @@
+#!/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):
+    """
+          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:
+            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:
+            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:
+        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:
+        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)
+