diff options
author | van Hauser <vh@thc.org> | 2019-06-19 19:45:05 +0200 |
---|---|---|
committer | van Hauser <vh@thc.org> | 2019-06-19 19:45:05 +0200 |
commit | 1d6e1ec61c556668951df4b9c88f93beb61fe1b8 (patch) | |
tree | 2ad14c4c7df4a031d7909184d9a8d452c4d2a986 | |
parent | db3cc11195af0dfae7c4bfcac78c75f48fbaa430 (diff) | |
download | afl++-1d6e1ec61c556668951df4b9c88f93beb61fe1b8.tar.gz |
Python 2.7 mutator module support added
-rw-r--r-- | Makefile | 53 | ||||
-rw-r--r-- | afl-fuzz.c | 525 | ||||
-rw-r--r-- | docs/ChangeLog | 3 | ||||
-rw-r--r-- | docs/env_variables.txt | 9 | ||||
-rw-r--r-- | docs/python_mutators.txt | 147 |
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. |