aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvan Hauser <vh@thc.org>2019-06-19 19:45:05 +0200
committervan Hauser <vh@thc.org>2019-06-19 19:45:05 +0200
commit1d6e1ec61c556668951df4b9c88f93beb61fe1b8 (patch)
tree2ad14c4c7df4a031d7909184d9a8d452c4d2a986
parentdb3cc11195af0dfae7c4bfcac78c75f48fbaa430 (diff)
downloadafl++-1d6e1ec61c556668951df4b9c88f93beb61fe1b8.tar.gz
Python 2.7 mutator module support added
-rw-r--r--Makefile53
-rw-r--r--afl-fuzz.c525
-rw-r--r--docs/ChangeLog3
-rw-r--r--docs/env_variables.txt9
-rw-r--r--docs/python_mutators.txt147
5 files changed, 713 insertions, 24 deletions
diff --git a/Makefile b/Makefile
index d05cd76c..1d8d819f 100644
--- a/Makefile
+++ b/Makefile
@@ -32,6 +32,8 @@ CFLAGS += -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign \
-DAFL_PATH=\"$(HELPER_PATH)\" -DDOC_PATH=\"$(DOC_PATH)\" \
-DBIN_PATH=\"$(BIN_PATH)\"
+PYTHON_INCLUDE ?= /usr/include/python2.7
+
ifneq "$(filter Linux GNU%,$(shell uname))" ""
LDFLAGS += -ldl
endif
@@ -44,7 +46,15 @@ endif
COMM_HDR = alloc-inl.h config.h debug.h types.h
-all: test_x86 $(PROGS) afl-as test_build all_done
+ifeq "$(shell echo '\#include <Python.h>XXXvoid main() {}' | sed 's/XXX/\n/g' | $(CC) -x c - -o .test -I$(PYTHON_INCLUDE) -lpython2.7 && echo 1 || echo 0 )" "1"
+ PYTHON_OK=1
+ PYFLAGS=-DUSE_PYTHON -I$(PYTHON_INCLUDE) -lpython2.7
+else
+ PYTHON_OK=0
+ PYFLAGS=
+endif
+
+all: test_x86 test_python27 $(PROGS) afl-as test_build all_done
ifndef AFL_NO_X86
@@ -61,6 +71,19 @@ test_x86:
endif
+ifeq "$(PYTHON_OK)" "1"
+
+test_python27:
+ @rm -f .test 2> /dev/null
+ @echo "[+] Python 2.7 support seems to be working."
+
+else
+
+test_python27:
+ @echo "[-] You seem to need to install the package python2.7-dev, but it is optional so we continue"
+
+endif
+
afl-gcc: afl-gcc.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
set -e; for i in afl-g++ afl-clang afl-clang++; do ln -sf afl-gcc $$i; done
@@ -70,7 +93,7 @@ afl-as: afl-as.c afl-as.h $(COMM_HDR) | test_x86
ln -sf afl-as as
afl-fuzz: afl-fuzz.c $(COMM_HDR) | test_x86
- $(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
+ $(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS) $(PYFLAGS)
afl-showmap: afl-showmap.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
@@ -142,16 +165,16 @@ endif
cp -r dictionaries/ $${DESTDIR}$(MISC_PATH)
publish: clean
- test "`basename $$PWD`" = "afl" || exit 1
- test -f ~/www/afl/releases/$(PROGNAME)-$(VERSION).tgz; if [ "$$?" = "0" ]; then echo; echo "Change program version in config.h, mmkay?"; echo; exit 1; fi
- cd ..; rm -rf $(PROGNAME)-$(VERSION); cp -pr $(PROGNAME) $(PROGNAME)-$(VERSION); \
- tar -cvz -f ~/www/afl/releases/$(PROGNAME)-$(VERSION).tgz $(PROGNAME)-$(VERSION)
- chmod 644 ~/www/afl/releases/$(PROGNAME)-$(VERSION).tgz
- ( cd ~/www/afl/releases/; ln -s -f $(PROGNAME)-$(VERSION).tgz $(PROGNAME)-latest.tgz )
- cat docs/README >~/www/afl/README.txt
- cat docs/status_screen.txt >~/www/afl/status_screen.txt
- cat docs/historical_notes.txt >~/www/afl/historical_notes.txt
- cat docs/technical_details.txt >~/www/afl/technical_details.txt
- cat docs/ChangeLog >~/www/afl/ChangeLog.txt
- cat docs/QuickStartGuide.txt >~/www/afl/QuickStartGuide.txt
- echo -n "$(VERSION)" >~/www/afl/version.txt
+# test "`basename $$PWD`" = "afl" || exit 1
+# test -f ~/www/afl/releases/$(PROGNAME)-$(VERSION).tgz; if [ "$$?" = "0" ]; then echo; echo "Change program version in config.h, mmkay?"; echo; exit 1; fi
+# cd ..; rm -rf $(PROGNAME)-$(VERSION); cp -pr $(PROGNAME) $(PROGNAME)-$(VERSION); \
+# tar -cvz -f ~/www/afl/releases/$(PROGNAME)-$(VERSION).tgz $(PROGNAME)-$(VERSION)
+# chmod 644 ~/www/afl/releases/$(PROGNAME)-$(VERSION).tgz
+# ( cd ~/www/afl/releases/; ln -s -f $(PROGNAME)-$(VERSION).tgz $(PROGNAME)-latest.tgz )
+# cat docs/README >~/www/afl/README.txt
+# cat docs/status_screen.txt >~/www/afl/status_screen.txt
+# cat docs/historical_notes.txt >~/www/afl/historical_notes.txt
+# cat docs/technical_details.txt >~/www/afl/technical_details.txt
+# cat docs/ChangeLog >~/www/afl/ChangeLog.txt
+# cat docs/QuickStartGuide.txt >~/www/afl/QuickStartGuide.txt
+# echo -n "$(VERSION)" >~/www/afl/version.txt
diff --git a/afl-fuzz.c b/afl-fuzz.c
index b775803d..ef95280a 100644
--- a/afl-fuzz.c
+++ b/afl-fuzz.c
@@ -98,6 +98,11 @@ static u32 hang_tmout = EXEC_TIMEOUT; /* Timeout used for hang det (ms) */
EXP_ST u64 mem_limit = MEM_LIMIT; /* Memory cap for child (MB) */
+EXP_ST u8 cal_cycles = CAL_CYCLES; /* Calibration cycles defaults */
+EXP_ST u8 cal_cycles_long = CAL_CYCLES_LONG;
+EXP_ST u8 debug, /* Debug mode */
+ python_only; /* Python-only mode */
+
static u32 stats_update_freq = 1; /* Stats update frequency (execs) */
enum {
@@ -313,7 +318,8 @@ enum {
/* 13 */ STAGE_EXTRAS_UI,
/* 14 */ STAGE_EXTRAS_AO,
/* 15 */ STAGE_HAVOC,
- /* 16 */ STAGE_SPLICE
+ /* 16 */ STAGE_SPLICE,
+ /* 17 */ STAGE_PYTHON
};
/* Stage value types */
@@ -336,6 +342,222 @@ enum {
};
+static inline u32 UR(u32 limit);
+
+/* Python stuff */
+#ifdef USE_PYTHON
+#include <Python.h>
+
+static PyObject *py_module;
+
+enum {
+ /* 00 */ PY_FUNC_INIT,
+ /* 01 */ PY_FUNC_FUZZ,
+ /* 02 */ PY_FUNC_INIT_TRIM,
+ /* 03 */ PY_FUNC_POST_TRIM,
+ /* 04 */ PY_FUNC_TRIM,
+ PY_FUNC_COUNT
+};
+
+static PyObject *py_functions[PY_FUNC_COUNT];
+
+static int init_py() {
+ Py_Initialize();
+ u8* module_name = getenv("AFL_PYTHON_MODULE");
+ u8 py_notrim = 0;
+
+ if (module_name) {
+ PyObject* py_name = PyString_FromString(module_name);
+
+ py_module = PyImport_Import(py_name);
+ Py_DECREF(py_name);
+
+ if (py_module != NULL) {
+ py_functions[PY_FUNC_INIT] = PyObject_GetAttrString(py_module, "init");
+ py_functions[PY_FUNC_FUZZ] = PyObject_GetAttrString(py_module, "fuzz");
+ py_functions[PY_FUNC_INIT_TRIM] = PyObject_GetAttrString(py_module, "init_trim");
+ py_functions[PY_FUNC_POST_TRIM] = PyObject_GetAttrString(py_module, "post_trim");
+ py_functions[PY_FUNC_TRIM] = PyObject_GetAttrString(py_module, "trim");
+
+ for (u8 py_idx = 0; py_idx < PY_FUNC_COUNT; ++py_idx) {
+ if (!py_functions[py_idx] || !PyCallable_Check(py_functions[py_idx])) {
+ if (py_idx >= PY_FUNC_INIT_TRIM && py_idx <= PY_FUNC_TRIM) {
+ // Implementing the trim API is optional for now
+ if (PyErr_Occurred())
+ PyErr_Print();
+ py_notrim = 1;
+ } else {
+ if (PyErr_Occurred())
+ PyErr_Print();
+ fprintf(stderr, "Cannot find/call function with index %d in external Python module.\n", py_idx);
+ return 1;
+ }
+ }
+
+ }
+
+ if (py_notrim) {
+ py_functions[PY_FUNC_INIT_TRIM] = NULL;
+ py_functions[PY_FUNC_POST_TRIM] = NULL;
+ py_functions[PY_FUNC_TRIM] = NULL;
+ WARNF("Python module does not implement trim API, standard trimming will be used.");
+ }
+
+ PyObject *py_args, *py_value;
+
+ /* Provide the init function a seed for the Python RNG */
+ py_args = PyTuple_New(1);
+ py_value = PyInt_FromLong(UR(0xFFFFFFFF));
+ if (!py_value) {
+ Py_DECREF(py_args);
+ fprintf(stderr, "Cannot convert argument\n");
+ return 1;
+ }
+
+ PyTuple_SetItem(py_args, 0, py_value);
+
+ py_value = PyObject_CallObject(py_functions[PY_FUNC_INIT], py_args);
+
+ Py_DECREF(py_args);
+
+ if (py_value == NULL) {
+ PyErr_Print();
+ fprintf(stderr,"Call failed\n");
+ return 1;
+ }
+ } else {
+ PyErr_Print();
+ fprintf(stderr, "Failed to load \"%s\"\n", module_name);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static void finalize_py() {
+ if (py_module != NULL) {
+ u32 i;
+ for (i = 0; i < PY_FUNC_COUNT; ++i)
+ Py_XDECREF(py_functions[i]);
+
+ Py_DECREF(py_module);
+ }
+
+ Py_Finalize();
+}
+
+static void fuzz_py(char* buf, size_t buflen, char* add_buf, size_t add_buflen, char** ret, size_t* retlen) {
+ PyObject *py_args, *py_value;
+
+ if (py_module != NULL) {
+ py_args = PyTuple_New(2);
+ py_value = PyByteArray_FromStringAndSize(buf, buflen);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ fprintf(stderr, "Cannot convert argument\n");
+ return;
+ }
+
+ PyTuple_SetItem(py_args, 0, py_value);
+
+ py_value = PyByteArray_FromStringAndSize(add_buf, add_buflen);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ fprintf(stderr, "Cannot convert argument\n");
+ return;
+ }
+
+ PyTuple_SetItem(py_args, 1, py_value);
+
+ py_value = PyObject_CallObject(py_functions[PY_FUNC_FUZZ], py_args);
+
+ Py_DECREF(py_args);
+
+ if (py_value != NULL) {
+ *retlen = PyByteArray_Size(py_value);
+ *ret = malloc(*retlen);
+ memcpy(*ret, PyByteArray_AsString(py_value), *retlen);
+ Py_DECREF(py_value);
+ } else {
+ PyErr_Print();
+ fprintf(stderr,"Call failed\n");
+ return;
+ }
+ }
+}
+static u32 init_trim_py(char* buf, size_t buflen) {
+ PyObject *py_args, *py_value;
+
+ py_args = PyTuple_New(1);
+ py_value = PyByteArray_FromStringAndSize(buf, buflen);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ FATAL("Failed to convert arguments");
+ }
+
+ PyTuple_SetItem(py_args, 0, py_value);
+
+ py_value = PyObject_CallObject(py_functions[PY_FUNC_INIT_TRIM], py_args);
+ Py_DECREF(py_args);
+
+ if (py_value != NULL) {
+ u32 retcnt = PyInt_AsLong(py_value);
+ Py_DECREF(py_value);
+ return retcnt;
+ } else {
+ PyErr_Print();
+ FATAL("Call failed");
+ }
+}
+static u32 post_trim_py(char success) {
+ PyObject *py_args, *py_value;
+
+ py_args = PyTuple_New(1);
+
+ py_value = PyBool_FromLong(success);
+ if (!py_value) {
+ Py_DECREF(py_args);
+ FATAL("Failed to convert arguments");
+ }
+
+ PyTuple_SetItem(py_args, 0, py_value);
+
+ py_value = PyObject_CallObject(py_functions[PY_FUNC_POST_TRIM], py_args);
+ Py_DECREF(py_args);
+
+ if (py_value != NULL) {
+ u32 retcnt = PyInt_AsLong(py_value);
+ Py_DECREF(py_value);
+ return retcnt;
+ } else {
+ PyErr_Print();
+ FATAL("Call failed");
+ }
+}
+
+static void trim_py(char** ret, size_t* retlen) {
+ PyObject *py_args, *py_value;
+
+ py_args = PyTuple_New(0);
+ py_value = PyObject_CallObject(py_functions[PY_FUNC_TRIM], py_args);
+ Py_DECREF(py_args);
+
+ if (py_value != NULL) {
+ *retlen = PyByteArray_Size(py_value);
+ *ret = malloc(*retlen);
+ memcpy(*ret, PyByteArray_AsString(py_value), *retlen);
+ Py_DECREF(py_value);
+ } else {
+ PyErr_Print();
+ FATAL("Call failed");
+ }
+}
+
+#endif /* USE_PYTHON */
+
+
+
/* Get unix time in milliseconds */
static u64 get_cur_time(void) {
@@ -2071,8 +2293,10 @@ EXP_ST void init_forkserver(char** argv) {
setsid();
- dup2(dev_null_fd, 1);
- dup2(dev_null_fd, 2);
+ if (!getenv("AFL_DEBUG_CHILD_OUTPUT")) {
+ dup2(dev_null_fd, 1);
+ dup2(dev_null_fd, 2);
+ }
if (out_file) {
@@ -3892,6 +4116,10 @@ static void maybe_delete_out_dir(void) {
fn = alloc_printf("%s/plot_data", out_dir);
if (unlink(fn) && errno != ENOENT) goto dir_cleanup_failed;
ck_free(fn);
+
+ fn = alloc_printf("%s/cmdline", out_dir);
+ if (unlink(fn) && errno != ENOENT) goto dir_cleanup_failed;
+ ck_free(fn);
OKF("Output dir cleanup successful.");
@@ -4294,9 +4522,10 @@ static void show_stats(void) {
" imported : " cRST "%-10s" bSTG bV "\n", tmp,
sync_id ? DI(queued_imported) : (u8*)"n/a");
- sprintf(tmp, "%s/%s, %s/%s",
+ sprintf(tmp, "%s/%s, %s/%s, %s/%s",
DI(stage_finds[STAGE_HAVOC]), DI(stage_cycles[STAGE_HAVOC]),
- DI(stage_finds[STAGE_SPLICE]), DI(stage_cycles[STAGE_SPLICE]));
+ DI(stage_finds[STAGE_SPLICE]), DI(stage_cycles[STAGE_SPLICE]),
+ DI(stage_finds[STAGE_PYTHON]), DI(stage_cycles[STAGE_PYTHON]));
SAYF(bV bSTOP " havoc : " cRST "%-36s " bSTG bV bSTOP, tmp);
@@ -4497,6 +4726,113 @@ static void show_init_stats(void) {
}
+#ifdef USE_PYTHON
+static u8 trim_case_python(char** argv, struct queue_entry* q, u8* in_buf) {
+
+ static u8 tmp[64];
+ static u8 clean_trace[MAP_SIZE];
+
+ u8 needs_write = 0, fault = 0;
+ u32 trim_exec = 0;
+ u32 orig_len = q->len;
+
+ stage_name = tmp;
+ bytes_trim_in += q->len;
+
+ /* Initialize trimming in the Python module */
+ stage_cur = 0;
+ stage_max = init_trim_py(in_buf, q->len);
+
+ if (not_on_tty && debug)
+ SAYF("[Python Trimming] START: Max %d iterations, %d bytes", stage_max, q->len);
+
+ while(stage_cur < stage_max) {
+ sprintf(tmp, "ptrim %s", DI(trim_exec));
+
+ u32 cksum;
+
+ char* retbuf = NULL;
+ size_t retlen = 0;
+
+ trim_py(&retbuf, &retlen);
+
+ if (retlen > orig_len)
+ FATAL("Trimmed data returned by Python module is larger than original data");
+
+ write_to_testcase(retbuf, retlen);
+
+ fault = run_target(argv, exec_tmout);
+ trim_execs++;
+
+ if (stop_soon || fault == FAULT_ERROR) goto abort_trimming;
+
+ cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
+
+ if (cksum == q->exec_cksum) {
+
+ q->len = retlen;
+ memcpy(in_buf, retbuf, retlen);
+
+ /* Let's save a clean trace, which will be needed by
+ update_bitmap_score once we're done with the trimming stuff. */
+
+ if (!needs_write) {
+
+ needs_write = 1;
+ memcpy(clean_trace, trace_bits, MAP_SIZE);
+
+ }
+
+ /* Tell the Python module that the trimming was successful */
+ stage_cur = post_trim_py(1);
+
+ if (not_on_tty && debug)
+ SAYF("[Python Trimming] SUCCESS: %d/%d iterations (now at %d bytes)", stage_cur, stage_max, q->len);
+ } else {
+ /* Tell the Python module that the trimming was unsuccessful */
+ stage_cur = post_trim_py(0);
+ if (not_on_tty && debug)
+ SAYF("[Python Trimming] FAILURE: %d/%d iterations", stage_cur, stage_max);
+ }
+
+ /* Since this can be slow, update the screen every now and then. */
+
+ if (!(trim_exec++ % stats_update_freq)) show_stats();
+ }
+
+ if (not_on_tty && debug)
+ SAYF("[Python Trimming] DONE: %d bytes -> %d bytes", orig_len, q->len);
+
+ /* If we have made changes to in_buf, we also need to update the on-disk
+ version of the test case. */
+
+ if (needs_write) {
+
+ s32 fd;
+
+ unlink(q->fname); /* ignore errors */
+
+ fd = open(q->fname, O_WRONLY | O_CREAT | O_EXCL, 0600);
+
+ if (fd < 0) PFATAL("Unable to create '%s'", q->fname);
+
+ ck_write(fd, in_buf, q->len, q->fname);
+ close(fd);
+
+ memcpy(trace_bits, clean_trace, MAP_SIZE);
+ update_bitmap_score(q);
+
+ }
+
+
+
+abort_trimming:
+
+ bytes_trim_out += q->len;
+ return fault;
+
+}
+#endif
/* Trim all new test cases to save cycles when doing deterministic checks. The
trimmer uses power-of-two increments somewhere between 1/16 and 1/1024 of
@@ -4504,6 +4840,11 @@ static void show_init_stats(void) {
static u8 trim_case(char** argv, struct queue_entry* q, u8* in_buf) {
+#ifdef USE_PYTHON
+ if (py_functions[PY_FUNC_TRIM])
+ return trim_case_python(argv, q, in_buf);
+#endif
+
static u8 tmp[64];
static u8 clean_trace[MAP_SIZE];
@@ -5044,7 +5385,7 @@ static u8 fuzz_one(char** argv) {
s32 len, fd, temp_len, i, j;
u8 *in_buf, *out_buf, *orig_in, *ex_tmp, *eff_map = 0;
- u64 havoc_queued, orig_hit_cnt, new_hit_cnt;
+ u64 havoc_queued = 0, orig_hit_cnt, new_hit_cnt;
u32 splice_cycle = 0, perf_score = 100, orig_perf, prev_cksum, eff_cnt = 1;
u8 ret_val = 1, doing_det = 0;
@@ -5106,7 +5447,7 @@ static u8 fuzz_one(char** argv) {
orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
- if (orig_in == MAP_FAILED) PFATAL("Unable to mmap '%s'", queue_cur->fname);
+ if (orig_in == MAP_FAILED) PFATAL("Unable to mmap '%s' with len %u", queue_cur->fname, len);
close(fd);
@@ -5190,13 +5531,21 @@ static u8 fuzz_one(char** argv) {
? queue_cur->depth * 30
: HAVOC_MAX_MULT * 100))
|| queue_cur->passed_det)
+#ifdef USE_PYTHON
+ goto python_stage;
+#else
goto havoc_stage;
+#endif
/* Skip deterministic fuzzing if exec path checksum puts this out of scope
for this master instance. */
if (master_max && (queue_cur->exec_cksum % master_max) != master_id - 1)
+#ifdef USE_PYTHON
+ goto python_stage;
+#else
goto havoc_stage;
+#endif
doing_det = 1;
@@ -6155,6 +6504,99 @@ skip_extras:
if (!queue_cur->passed_det) mark_as_det_done(queue_cur);
+#ifdef USE_PYTHON
+python_stage:
+ /**********************************
+ * EXTERNAL MUTATORS (Python API) *
+ **********************************/
+
+ if (!py_module) goto havoc_stage;
+
+ stage_name = "python";
+ stage_short = "python";
+ stage_max = HAVOC_CYCLES * perf_score / havoc_div / 100;
+
+ if (stage_max < HAVOC_MIN) stage_max = HAVOC_MIN;
+
+ orig_hit_cnt = queued_paths + unique_crashes;
+
+ char* retbuf = NULL;
+ size_t retlen = 0;
+
+ for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
+ struct queue_entry* target;
+ u32 tid;
+ u8* new_buf;
+
+retry_external_pick:
+ /* Pick a random other queue entry for passing to external API */
+ do { tid = UR(queued_paths); } while (tid == current_entry && queued_paths > 1);
+
+ target = queue;
+
+ while (tid >= 100) { target = target->next_100; tid -= 100; }
+ while (tid--) target = target->next;
+
+ /* Make sure that the target has a reasonable length. */
+
+ while (target && (target->len < 2 || target == queue_cur) && queued_paths > 1) {
+ target = target->next;
+ splicing_with++;
+ }
+
+ if (!target) goto retry_external_pick;
+
+ /* Read the additional testcase into a new buffer. */
+ fd = open(target->fname, O_RDONLY);
+ if (fd < 0) PFATAL("Unable to open '%s'", target->fname);
+ new_buf = ck_alloc_nozero(target->len);
+ ck_read(fd, new_buf, target->len, target->fname);
+ close(fd);
+
+ fuzz_py(out_buf, len, new_buf, target->len, &retbuf, &retlen);
+
+ ck_free(new_buf);
+
+ if (retbuf) {
+ if (!retlen)
+ goto abandon_entry;
+
+ if (common_fuzz_stuff(argv, retbuf, retlen)) {
+ free(retbuf);
+ goto abandon_entry;
+ }
+
+ /* Reset retbuf/retlen */
+ free(retbuf);
+ retbuf = NULL;
+ retlen = 0;
+
+ /* If we're finding new stuff, let's run for a bit longer, limits
+ permitting. */
+
+ if (queued_paths != havoc_queued) {
+ if (perf_score <= HAVOC_MAX_MULT * 100) {
+ stage_max *= 2;
+ perf_score *= 2;
+ }
+
+ havoc_queued = queued_paths;
+ }
+ }
+ }
+
+ new_hit_cnt = queued_paths + unique_crashes;
+
+ stage_finds[STAGE_PYTHON] += new_hit_cnt - orig_hit_cnt;
+ stage_cycles[STAGE_PYTHON] += stage_max;
+
+ if (python_only) {
+ /* Skip other stages */
+ ret_val = 0;
+ goto abandon_entry;
+ }
+#endif
+
/****************
* RANDOM HAVOC *
****************/
@@ -6699,7 +7141,11 @@ retry_splicing:
out_buf = ck_alloc_nozero(len);
memcpy(out_buf, in_buf, len);
+#ifdef USE_PYTHON
+ goto python_stage;
+#else
goto havoc_stage;
+#endif
}
@@ -7180,6 +7626,9 @@ static void usage(u8* argv0) {
" -C - crash exploration mode (the peruvian rabbit thing)\n"
" -e ext - File extension for the temporarily generated test case\n\n"
+#ifdef USE_PYTHON
+ "Compiled with Python 2.7 module support, see docs/python_mutators.txt\n"
+#endif
"For additional tips, please consult %s/README.\n\n",
argv0, EXEC_TIMEOUT, MEM_LIMIT, doc_path);
@@ -7311,6 +7760,30 @@ EXP_ST void setup_dirs_fds(void) {
}
+static void setup_cmdline_file(char** argv) {
+ u8* tmp;
+ s32 fd;
+ u32 i = 0;
+
+ FILE* cmdline_file = NULL;
+
+ /* Store the command line to reproduce our findings */
+ tmp = alloc_printf("%s/cmdline", out_dir);
+ fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
+ if (fd < 0) PFATAL("Unable to create '%s'", tmp);
+ ck_free(tmp);
+
+ cmdline_file = fdopen(fd, "w");
+ if (!cmdline_file) PFATAL("fdopen() failed");
+
+ while (argv[i]) {
+ fprintf(cmdline_file, "%s\n", argv[i]);
+ i++;
+ }
+
+ fclose(cmdline_file);
+}
+
/* Setup the output file for fuzzed data, if not using -f. */
@@ -7557,8 +8030,8 @@ static void fix_up_sync(void) {
if (force_deterministic)
FATAL("use -S instead of -M -d");
- else
- FATAL("-S already implies -d");
+ //else
+ // FATAL("-S already implies -d");
}
@@ -8116,6 +8589,23 @@ int main(int argc, char** argv) {
check_if_tty();
+ if (getenv("AFL_CAL_FAST")) {
+ /* Use less calibration cycles, for slow applications */
+ cal_cycles = 3;
+ cal_cycles_long = 5;
+ }
+
+ if (getenv("AFL_DEBUG"))
+ debug = 1;
+
+ if (getenv("AFL_PYTHON_ONLY")) {
+ /* This ensures we don't proceed to havoc/splice */
+ python_only = 1;
+
+ /* Ensure we also skip all deterministic steps */
+ skip_deterministic = 1;
+ }
+
get_core_count();
#ifdef HAVE_AFFINITY
@@ -8130,6 +8620,19 @@ int main(int argc, char** argv) {
init_count_class16();
setup_dirs_fds();
+
+ u8 with_python_support = 0;
+#ifdef USE_PYTHON
+ if (init_py())
+ FATAL("Failed to initialize Python module");
+ with_python_support = 1;
+#endif
+
+ if (getenv("AFL_PYTHON_MODULE") && !with_python_support)
+ FATAL("Your AFL binary was built without Python support");
+
+ setup_cmdline_file(argv + optind);
+
read_testcases();
load_auto();
@@ -8262,6 +8765,10 @@ stop_fuzzing:
alloc_report();
+#ifdef USE_PYTHON
+ finalize_py();
+#endif
+
OKF("We're done here. Have a nice day!\n");
exit(0);
diff --git a/docs/ChangeLog b/docs/ChangeLog
index 3d93821f..616dd78b 100644
--- a/docs/ChangeLog
+++ b/docs/ChangeLog
@@ -17,6 +17,9 @@ sending a mail to <afl-users+subscribe@googlegroups.com>.
Version ++2.52d (tbd):
-----------------------------
+ - 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
- ... your idea or patch?
diff --git a/docs/env_variables.txt b/docs/env_variables.txt
index 8c9bfa95..07a74dc0 100644
--- a/docs/env_variables.txt
+++ b/docs/env_variables.txt
@@ -68,6 +68,15 @@ tools make fairly broad use of environmental variables:
- Setting AFL_QUIET will prevent afl-cc and afl-as banners from being
displayed during compilation, in case you find them distracting.
+ - Setting AFL_CAL_FAST will speed up the initial calibration, if the
+ application is very slow
+
+ - Setting AFL_DEBUG_CHILD_OUTPUT will not suppress the child output.
+ Not pretty but good for debugging purposes.
+
+ - For AFL_PYTHON_MODULE and AFL_PYTHON_ONLY - they require to be compiled
+ with -DUSE_PYTHON. Please see docs/python_mutators.txt
+
2) Settings for afl-clang-fast
------------------------------
diff --git a/docs/python_mutators.txt b/docs/python_mutators.txt
new file mode 100644
index 00000000..04fd7208
--- /dev/null
+++ b/docs/python_mutators.txt
@@ -0,0 +1,147 @@
+==================================================
+Adding custom mutators to AFL using Python modules
+==================================================
+
+ This file describes how you can utilize the external Python API to write
+ your own custom mutation routines.
+
+ Note: This feature is highly experimental. Use at your own risk.
+
+ Implemented by Christian Holler (:decoder) <choller@mozilla.com>.
+
+ NOTE: This is for Python 2.7 !
+ Anyone who wants to add Python 3.7 support is happily welcome :)
+
+
+1) Description and purpose
+--------------------------
+
+While AFLFuzz comes with a good selection of generic deterministic and
+non-deterministic mutation operations, it sometimes might make sense to extend
+these to implement strategies more specific to the target you are fuzzing.
+
+For simplicity and in order to allow people without C knowledge to extend
+AFLFuzz, I implemented a "Python" stage that can make use of an external
+module (written in Python) that implements a custom mutation stage.
+
+The main motivation behind this is to lower the barrier for people
+experimenting with this tool. Hopefully, someone will be able to do useful
+things with this extension.
+
+If you find it useful, have questions or need additional features added to the
+interface, feel free to send a mail to <choller@mozilla.com>.
+
+See the following information to get a better pictures:
+ https://www.agarri.fr/docs/XML_Fuzzing-NullCon2017-PUBLIC.pdf
+ https://bugs.chromium.org/p/chromium/issues/detail?id=930663
+
+
+2) How the Python module looks like
+-----------------------------------
+
+You can find a simple example in pymodules/example.py including documentation
+explaining each function. In the same directory, you can find another simple
+module that performs simple mutations.
+
+Right now, "init" is called at program startup and can be used to perform any
+kinds of one-time initializations while "fuzz" is called each time a mutation
+is requested.
+
+There is also optional support for a trimming API, see the section below for
+further information about this feature.
+
+3) How to compile AFLFuzz with Python support
+---------------------------------------------
+
+You must install the python 2.7 development package of your Linux distribution
+before this will work. On Debian/Ubuntu/Kali this can be done with:
+ apt install python2.7-dev
+
+A prerequisite for using this mode is to compile AFLFuzz with Python support.
+
+The afl Makefile performs some magic and detects Python 2.7 if it is in the
+default path and compiles afl-fuzz with the feature if available (which is
+/usr/include/python2.7 for the Python.h include and /usr/lib/x86_64-linux-gnu
+for the libpython2.7.a library)
+
+In case your setup is different set the necessary variables like this:
+PYTHON_INCLUDE=/path/to/python2.7/include LDFLAGS=-L/path/to/python2.7/lib make
+
+
+4) How to run AFLFuzz with your custom module
+---------------------------------------------
+
+You must pass the module name inside the env variable AFL_PYTHON_MODULE.
+
+In addition, if you are trying to load the module from the local directory,
+you must adjust your PYTHONPATH to reflect this circumstance. The following
+command should work if you are inside the aflfuzz directory:
+
+$ AFL_PYTHON_MODULE="pymodules.test" PYTHONPATH=. ./afl-fuzz
+
+Optionally, the following environment variables are supported:
+
+AFL_PYTHON_ONLY - Disable all other mutation stages. This can prevent broken
+ testcases (those that your Python module can't work with
+ anymore) to fill up your queue. Best combined with a custom
+ trimming routine (see below) because trimming can cause the
+ same test breakage like havoc and splice.
+
+AFL_DEBUG - When combined with AFL_NO_UI, this causes the C trimming code
+ to emit additional messages about the performance and actions
+ of your custom Python trimmer. Use this to see if it works :)
+
+5) Order and statistics
+-----------------------
+
+The Python stage is set to be the first non-deterministic stage (right before
+the havoc stage). In the statistics however, it shows up as the third number
+under "havoc". That's because I'm lazy and I didn't want to mess with the UI
+too much ;)
+
+6) Trimming support
+-------------------
+
+The generic trimming routines implemented in AFLFuzz can easily destroy the
+structure of complex formats, possibly leading to a point where you have a lot
+of testcases in the queue that your Python module cannot process anymore but
+your target application still accepts. This is especially the case when your
+target can process a part of the input (causing coverage) and then errors out
+on the remaining input.
+
+In such cases, it makes sense to implement a custom trimming routine in Python.
+The API consists of multiple methods because after each trimming step, we have
+to go back into the C code to check if the coverage bitmap is still the same
+for the trimmed input. Here's a quick API description:
+
+init_trim: 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.
+
+trim: 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.
+
+post_trim: 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.
+ In any case, this method must return the next trim iteration index
+ (from 0 to the maximum amount of steps you returned in init_trim).
+
+Omitting any of the methods will cause Python trimming to be disabled and
+trigger a fallback to the builtin default trimming routine.