diff --git a/Makefile b/Makefile index ee5bde55..680752c8 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,8 @@ HELPER_PATH = $(PREFIX)/lib/afl DOC_PATH = $(PREFIX)/share/doc/afl MISC_PATH = $(PREFIX)/share/afl -# PROGS intentionally omit afl-as, which gets installed to its own dir. +# PROGS intentionally omit afl-as and libdislocator.so, which get installed +# to a different location. PROGS = afl-gcc afl-fuzz afl-showmap afl-tmin afl-gotcpu afl-analyze SH_PROGS = afl-plot afl-cmin afl-whatsup @@ -44,7 +45,7 @@ endif COMM_HDR = alloc-inl.h config.h debug.h types.h -all: test_x86 $(PROGS) afl-as test_build all_done +all: test_x86 $(PROGS) afl-as libdislocator.so test_build all_done ifndef AFL_NO_X86 @@ -84,6 +85,9 @@ afl-analyze: afl-analyze.c $(COMM_HDR) | test_x86 afl-gotcpu: afl-gotcpu.c $(COMM_HDR) | test_x86 $(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS) +libdislocator.so: libdislocator.so.c $(COMM_HDR) | test_x86 + $(CC) $(CFLAGS) -shared $@.c -o $@ $(LDFLAGS) + ifndef AFL_NO_X86 test_build: afl-gcc afl-as afl-showmap @@ -111,7 +115,7 @@ all_done: test_build .NOTPARALLEL: clean clean: - rm -f $(PROGS) afl-as as afl-g++ afl-clang afl-clang++ *.o *~ a.out core core.[1-9][0-9]* *.stackdump test .test test-instr .test-instr0 .test-instr1 qemu_mode/qemu-2.3.0.tar.bz2 afl-qemu-trace + rm -f $(PROGS) libdislocator.so afl-as as afl-g++ afl-clang afl-clang++ *.o *~ a.out core core.[1-9][0-9]* *.stackdump test .test test-instr .test-instr0 .test-instr1 qemu_mode/qemu-2.3.0.tar.bz2 afl-qemu-trace rm -rf out_dir qemu_mode/qemu-2.3.0 $(MAKE) -C llvm_mode clean @@ -126,6 +130,7 @@ install: all if [ -f afl-llvm-rt-64.o ]; then set -e; install -m 755 afl-llvm-rt-64.o $${DESTDIR}$(HELPER_PATH); fi set -e; for i in afl-g++ afl-clang afl-clang++; do ln -sf afl-gcc $${DESTDIR}$(BIN_PATH)/$$i; done install -m 755 afl-as $${DESTDIR}$(HELPER_PATH) + install -m 755 libdislocator.so $${DESTDIR}$(HELPER_PATH) ln -sf afl-as $${DESTDIR}$(HELPER_PATH)/as install -m 644 docs/README docs/ChangeLog docs/*.txt $${DESTDIR}$(DOC_PATH) cp -r testcases/ $${DESTDIR}$(MISC_PATH) diff --git a/config.h b/config.h index 18beddbc..ed56d60d 100644 --- a/config.h +++ b/config.h @@ -21,7 +21,7 @@ /* Version string: */ -#define VERSION "2.23b" +#define VERSION "2.24b" /****************************************************** * * diff --git a/docs/ChangeLog b/docs/ChangeLog index 8f1cd7f1..d046f0e0 100644 --- a/docs/ChangeLog +++ b/docs/ChangeLog @@ -16,6 +16,14 @@ Not sure if you should upgrade? The lowest currently recommended version is 2.21b. If you're stuck on an earlier release, it's strongly advisable to get on with the times. +-------------- +Version 2.24b: +-------------- + + - Added libdislocator.so, an experimental, abusive allocator. Try + it out with AFL_LD_PRELOAD=/path/to/libdislocator.so when running + afl-fuzz. + -------------- Version 2.23b: -------------- diff --git a/docs/README b/docs/README index 85ae16b4..a3c424be 100644 --- a/docs/README +++ b/docs/README @@ -115,7 +115,9 @@ $ CC=/path/to/afl/afl-gcc ./configure --disable-shared Setting AFL_HARDEN=1 when calling 'make' will cause the CC wrapper to automatically enable code hardening options that make it easier to detect -simple memory bugs. +simple memory bugs. Preloading 'libdislocator.so' (an abusive allocator) can +help uncover heap corruption isses, too; see libdislocator.so.c for info and +usage tips. PS. ASAN users are advised to review notes_for_asan.txt file for important caveats. diff --git a/docs/env_variables.txt b/docs/env_variables.txt index e17464f2..7777b933 100644 --- a/docs/env_variables.txt +++ b/docs/env_variables.txt @@ -139,7 +139,8 @@ checks or alter some of the more exotic semantics of the tool: - In QEMU mode (-Q), AFL_PATH will be searched for afl-qemu-trace. - Setting AFL_LD_PRELOAD causes AFL to set LD_PRELOAD for the target binary - without disrupting the afl-fuzz process itself. + without disrupting the afl-fuzz process itself. This is useful, among other + things, for bootstrapping libdislocator.so. - If you are Jakub, you may need AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES. Others need not apply. @@ -183,7 +184,23 @@ Virtually nothing to play with. Well, in QEMU mode (-Q), AFL_PATH will be searched for afl-qemu-trace. In addition to this, TMPDIR may be used if a temporary file can't be created in the current working directory. -7) Third-party variables set by afl-fuzz & other tools +7) Settings for libdislocator.so +-------------------------------- + +The library honors three environmental variables: + + - AFL_LD_LIMIT_MB caps the size of the maximum heap usage permitted by the + library, in megabytes. The default value is 1 GB. Once this is exceeded, + allocations will return NULL. + + - AFL_LD_LIMIT_HARD alters the behavior by calling abort() on excessive + allocations, thus causing what AFL would perceive as a crash. Useful for + programs that are supposed to maintain a specific memory footprint. + + - AFL_LD_VERBOSE causes the library to output some diagnostic messages + that may be useful for pinpointing the cause of any observed issues. + +8) Third-party variables set by afl-fuzz & other tools ------------------------------------------------------ Several variables are not directly interpreted by afl-fuzz, but are set to diff --git a/docs/status_screen.txt b/docs/status_screen.txt index 8068f93e..36e0713b 100644 --- a/docs/status_screen.txt +++ b/docs/status_screen.txt @@ -333,11 +333,12 @@ there are several things to look at: - Replace pthreads with GNU Pth (https://www.gnu.org/software/pth/), which allows you to use a deterministic scheduler. - - In persistent mode, minor reduction in the "stability" metric can be - normal, because not all the code behaves identically when re-entered; but - major drops may signify that the code within __AFL_LOOP() not working + - In persistent mode, minor drops in the "stability" metric can be normal, + because not all the code behaves identically when re-entered; but major + dips may signify that the code within __AFL_LOOP() is not behaving correctly on subsequent iterations (e.g., due to incomplete clean-up or - reinitialization of the state). + reinitialization of the state) and that most of the fuzzing effort goes + to waste. The paths where variable behavior is detected are marked with a matching entry in the /queue/.state/variable_behavior/ directory, so you can look diff --git a/libdislocator.so.c b/libdislocator.so.c new file mode 100644 index 00000000..27c009c8 --- /dev/null +++ b/libdislocator.so.c @@ -0,0 +1,290 @@ +/* + + american fuzzy lop - dislocator, an abusive allocator + ----------------------------------------------------- + + Written and maintained by Michal Zalewski + + Copyright 2016 Google Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + + This is a companion library that can be used as a drop-in replacement + for the libc allocator in the fuzzed binaries. It improves the odds of + bumping into heap-related security bugs in several ways: + + - It allocates all buffers so that they are immediately adjacent to a + subsequent PROT_NONE page, causing most off-by-one reads and writes + to immediately segfault, + + - It adds a canary immediately below the allocated buffer, to catch + writes to negative offsets (won't catch reads, though), + + - It sets the memory returned by malloc() to garbage values, improving + the odds of crashing when the target accesses uninitialized data, + + - It sets freed memory to PROT_NONE and does not actually reuse it, + causing most use-after-free bugs to segfault right away, + + - It forces all realloc() calls to return a new address - and sets + PROT_NONE on the original block. This catches use-after-realloc bugs, + + - It checks for calloc() overflows and can cause soft or hard failures + of alloc requests past a configurable memory limit (AFL_LD_LIMIT_MB, + AFL_LD_HARD_LIMIT). + + Basically, it is inspired by some of the non-default options available + for the OpenBSD allocator - see malloc.conf(5) on that platform for + reference. It is also somewhat similar to several other debugging + libraries, such as gmalloc and DUMA, but is simple, plug-and-play, and + designed specifically for fuzzing jobs. + + Note that it does nothing for stack-based memory handling errors. The + -fstack-protector-all setting for GCC / clang, enabled when using + AFL_HARDEN, can catch some subset of that. + + The allocator is slow and memory-intensive (even the tiniest allocation + uses up 4 kB of physical memory and 8 kB of virtual mem), making it + completely unsuitable for "production" uses; but it is faster and more + hassle-free than ASAN / MSAN when fuzzing small, self-contained binaries. + + To use this library, run AFL like so: + + AFL_LD_PRELOAD=/path/to/libdislocator.so ./afl-fuzz [...other params...] + + You *have* to specify path, even if it's just ./libdislocator.so or + $PWD/libdislocator.so. + + Similarly to afl-tmin, the library is not "proprietary" and can be + used with other fuzzers or testing tools without the need for any code + tweaks. + + Note that the LD_PRELOAD approach will work only if the target binary is + dynamically linked. + + */ + +#include +#include +#include +#include +#include + +#include "config.h" +#include "types.h" + +#ifndef PAGE_SIZE +# define PAGE_SIZE 4096 +#endif /* !PAGE_SIZE */ + +/* Error / message handling: */ + +#define DEBUGF(_x...) do { \ + if (alloc_verbose) { \ + fprintf(stderr, "[AFL] " _x); \ + fprintf(stderr, "\n"); \ + } \ + } while (0) + +#define FATAL(_x...) do { \ + fprintf(stderr, "*** [AFL] " _x); \ + fprintf(stderr, " ***\n"); \ + abort(); \ + } while (0) + +/* Macro to count the number of pages needed to store a buffer: */ + +#define PG_COUNT(_l) (((_l) + (PAGE_SIZE - 1)) / PAGE_SIZE) + +/* Canary & clobber bytes: */ + +#define ALLOC_CANARY 0xAACCAACC +#define ALLOC_CLOBBER 0x41 + +#define PTR_C(_p) (((u32*)(_p))[-1]) +#define PTR_L(_p) (((u32*)(_p))[-2]) + +/* Configurable stuff (use AFL_DISLOC_* to set): */ + +static u32 max_mem = MAX_ALLOC; /* Max heap usage to permit */ +static u8 alloc_verbose, /* Additional debug messages */ + hard_limit; /* abort() when max_mem exceeded? */ + +static __thread u64 total_mem; /* Currently allocated mem */ + +/* This is the main alloc function. It allocates one page more than necessary, + sets that tailing page to PROT_NONE, and then increments the return address + so that it is right-aligned to that boundary. Since it always uses mmap(), + the returned memory will be zeroed. */ + +static void* __dislocator_alloc(size_t len) { + + void* ret; + + if (total_mem + len > max_mem) { + + if (hard_limit) + FATAL("total allocs exceed %u MB", max_mem / 1024 / 1024); + + DEBUGF("total allocs exceed %u MB, returning NULL", + max_mem / 1024 / 1024); + + return NULL; + + } + + /* We will also store buffer length and a canary below the actual buffer, so + let's add 8 bytes for that. */ + + ret = mmap(NULL, (1 + PG_COUNT(len + 8)) * PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + if (ret == (void*)-1) { + + DEBUGF("mmap() failed when allocating memory (OOM?)"); + return NULL; + + } + + /* Set PROT_NONE on the last page. */ + + if (mprotect(ret + PG_COUNT(len + 8) * PAGE_SIZE, PAGE_SIZE, PROT_NONE)) + FATAL("mprotect() failed when allocating memory"); + + /* Offset the return pointer so that it's right-aligned to the page + boundary. */ + + ret += PAGE_SIZE * PG_COUNT(len + 8) - len - 8; + + /* Store allocation metadata. */ + + ret += 8; + + PTR_L(ret) = len; + PTR_C(ret) = ALLOC_CANARY; + + total_mem += len; + + return ret; + +} + + +/* The "user-facing" wrapper for calloc(). This just checks for overflows and + displays debug messages if requested. */ + +void* calloc(size_t elem_len, size_t elem_cnt) { + + void* ret; + + size_t len = elem_len * elem_cnt; + + /* Perform some sanity checks to detect obvious issues... */ + + if (elem_cnt && len / elem_cnt != elem_len) + FATAL("calloc(%zu, %zu) would overflow", elem_len, elem_cnt); + + ret = __dislocator_alloc(len); + + DEBUGF("calloc(%zu, %zu) = %p [%llu total]", elem_len, elem_cnt, ret, + total_mem); + + return ret; + +} + + +/* The wrapper for malloc(). Roughly the same, also clobbers the returned + memory (unlike calloc(), malloc() is not guaranteed to return zeroed + memory). */ + +void* malloc(size_t len) { + + void* ret; + + ret = __dislocator_alloc(len); + + DEBUGF("malloc(%zu) = %p [%llu total]", len, ret, total_mem); + + if (ret && len) memset(ret, ALLOC_CLOBBER, len); + + return ret; + +} + + +/* The wrapper for free(). This simply marks the entire region as PROT_NONE. + If the region is already freed, the code segfault during the attempt to + read the canary. Not very graceful, but works, right? */ + +void free(void* ptr) { + + u32 len; + + DEBUGF("free(%p)", ptr); + + if (!ptr) return; + + if (PTR_C(ptr) != ALLOC_CANARY) FATAL("bad allocator canary on free()"); + + len = PTR_L(ptr); + + total_mem -= len; + + /* Protect everything. Note that the extra page at the end is already + set as PROT_NONE, so we don't need to touch that. */ + + ptr -= PAGE_SIZE * PG_COUNT(len + 8) - len - 8; + + if (mprotect(ptr - 8, PG_COUNT(len + 8) * PAGE_SIZE, PROT_NONE)) + FATAL("mprotect() failed when freeing memory"); + + /* Keep the mapping; this is wasteful, but prevents ptr reuse. */ + +} + + +/* Realloc is pretty straightforward, too. We forcibly reallocate the buffer, + move data, and then free (aka mprotect()) the original one. */ + +void* realloc(void* ptr, size_t len) { + + void* ret; + + ret = malloc(len); + + if (ret && ptr) { + + if (PTR_C(ptr) != ALLOC_CANARY) FATAL("bad allocator canary on realloc()"); + + memcpy(ret, ptr, MIN(len, PTR_L(ptr))); + free(ptr); + + } + + DEBUGF("realloc(%p, %zu) = %p [%llu total]", ptr, len, ret, total_mem); + + return ret; + +} + + +__attribute__((constructor)) void __dislocator_init(void) { + + u8* tmp = getenv("AFL_LD_LIMIT_MB"); + + if (tmp) { + + max_mem = atoi(tmp) * 1024 * 1024; + if (!max_mem) FATAL("Bad value for AFL_LD_LIMIT_MB"); + + } + + alloc_verbose = !!getenv("AFL_LD_VERBOSE"); + hard_limit = !!getenv("AFL_LD_HARD_LIMIT"); + +} diff --git a/types.h b/types.h index dd8ede19..21d32da6 100644 --- a/types.h +++ b/types.h @@ -44,7 +44,7 @@ typedef uint32_t u32; typedef unsigned long long u64; #else typedef uint64_t u64; -#endif /* ^sizeof(...) */ +#endif /* ^__x86_64__ */ typedef int8_t s8; typedef int16_t s16;