Fuzzing, which is simply providing potentially invalid, unexpected, or random data as an input to a program, is an extremely effective way of finding bugs in large software systems, and is an important part of the software development life cycle.
Android's build system supports fuzzing through the inclusion of the libFuzzer project from the LLVM compiler infrastructure project. LibFuzzer is linked with the function under test and handles all input selection, mutation, and crash reporting that occurs during a fuzzing session. LLVM's sanitizers are used to aid in memory corruption detection and code coverage metrics.
This article provides an introduction to libFuzzer on Android and how to perform an instrumented build. It also includes instructions to write, run, and customize fuzzers.
To ensure you have a working image running on a device, follow the setup and build examples below.
After you flash your device with a standard Android build, follow the
instructions to flash an
AddressSanitizer
build, and turn on coverage by using SANITIZE_TARGET='address
coverage'
instead of SANITIZE_TARGET='address'
.
This example assumes the target device is Pixel (sailfish
) and is
already prepared for USB debugging (aosp_sailfish-userdebug
).
mkdir ~/bin
export PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo
repo init -u https://android.googlesource.com/platform/manifest -b master
repo sync -c -j8
wget https://dl.google.com/dl/android/aosp/google_devices-sailfish-nde63p-c36cb625.tgz
tar xvf google_devices-sailfish-nde63p-c36cb625.tgz
extract-google_devices-sailfish.sh
wget https://dl.google.com/dl/android/aosp/qcom-sailfish-nde63p-50a5f1e0.tgz
tar xvf qcom-sailfish-nde63p-50a5f1e0.tgz
extract-qcom-sailfish.sh
. build/envsetup.sh
lunch aosp_sailfish-userdebug
There is a two-step build process to create an instrumented system image that allows for reproducible fuzzing sessions.
First perform a full build of Android and flash it to the device. Next, build the instrumented version of Android using the existing build as a starting point. The build system is sophisticated enough to build only the required binaries and put them in the correct location.
make -j$(nproc)
-w
option erases userdata, ensuring a clean initial
state.)
fastboot oem unlock
fastboot flashall -w
make -j$(nproc) SANITIZE_TARGET='address coverage'
fastboot flash userdata
fastboot flashall
The target device should now be ready for libFuzzer fuzzing. To ensure your
build is an instrumented build, check for the existence of
/data/asan/lib
using adb as root:
adb root
adb shell ls -ld /data/asan/lib* drwxrwx--x 6 system system 8192 2016-10-05 14:52 /data/asan/lib drwxrwx--x 6 system system 8192 2016-10-05 14:52 /data/asan/lib64
These directories do not exist on a regular, non-instrumented build.
To illustrate writing an end-to-end fuzzer using libFuzzer in Android, use the following vulnerable code as a test case. This helps to test the fuzzer, ensure everything is working correctly, and illustrate what crash data looks like.
Here is the test function.
#include <stdint.h> #include <stddef.h> bool FuzzMe(const uint8_t *Data, size_t DataSize) { return DataSize >= 3 && Data[0] == 'F' && Data[1] == 'U' && Data[2] == 'Z' && Data[3] == 'Z'; // ← Out of bounds access }
To build and run this test fuzzer:
tools/fuzzers/fuzz_me_fuzzer
. The following files will all be
created in this directory.extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) { FuzzMe(buf, len); return 0; }
Android.mk
file:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_SRC_FILES := fuzz_me_fuzzer.cpp LOCAL_CFLAGS += -Wno-multichar -g -O0 LOCAL_MODULE_TAGS := optional LOCAL_CLANG := true LOCAL_MODULE:= fuzz_me_fuzzer Include $(BUILD_FUZZ_TEST)
Most of the logic to get this working is included in the BUILD_FUZZ_TEST macro,
which is defined in build/core/fuzz_test.mk.
make -j$(nproc) fuzz_me_fuzzer SANITIZE_TARGET="address coverage"
After following these steps, you should have a built fuzzer. The default
location for the fuzzer (for this example Pixel build) is
out/target/product/sailfish/data/nativetest/fuzzers/fuzz_me_fuzzer/fuzz_me_fuzzer
After you've built your fuzzer, upload the fuzzer and the vulnerable library to link against.
adb root
adb shell mkdir -p /data/tmp/fuzz_me_fuzzer/corpus
adb push $OUT/data/asan/nativetest/fuzzers/fuzz_me_fuzzer/fuzz_me_fuzzer /data/tmp/fuzz_me_fuzzer/
adb shell /data/tmp/fuzz_me_fuzzer/fuzz_me_fuzzer /data/tmp/fuzz_me_fuzzer/corpus
This results in output similar to the example output below.
INFO: Seed: 702890555 INFO: Loaded 1 modules (9 guards): [0xaaac6000, 0xaaac6024), Loading corpus dir: /data/tmp/fuzz_me_fuzzer/corpus INFO: -max_len is not provided, using 64 INFO: A corpus is not provided, starting from an empty corpus #0 READ units: 1 #1 INITED cov: 5 ft: 3 corp: 1/1b exec/s: 0 rss: 11Mb #6 NEW cov: 6 ft: 4 corp: 2/62b exec/s: 0 rss: 11Mb L: 61 MS: 1 InsertRepeatedBytes- #3008 NEW cov: 7 ft: 5 corp: 3/67b exec/s: 0 rss: 11Mb L: 5 MS: 1 CMP- DE: "F\x00\x00\x00"- #7962 NEW cov: 8 ft: 6 corp: 4/115b exec/s: 0 rss: 11Mb L: 48 MS: 1 InsertRepeatedBytes- #35324 NEW cov: 9 ft: 7 corp: 5/163b exec/s: 0 rss: 13Mb L: 48 MS: 1 ChangeBinInt- ================================================================= ==28219==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xe6423fb3 at pc 0xaaaae938 bp 0xffa31ab0 sp 0xffa31aa8 READ of size 1 at 0xe6423fb3 thread T0 #0 0xef72f6df in __sanitizer_print_stack_trace [asan_rtl] (discriminator 1) #1 0xaaab813d in fuzzer::Fuzzer::CrashCallback() external/llvm/lib/Fuzzer/FuzzerLoop.cpp:251 #2 0xaaab811b in fuzzer::Fuzzer::StaticCrashSignalCallback() external/llvm/lib/Fuzzer/FuzzerLoop.cpp:240 #3 0xef5a9a2b in $a.0 /proc/self/cwd/bionic/libc/arch-arm/bionic/__restore.S:48 #4 0xef5dba37 in tgkill /proc/self/cwd/bionic/libc/arch-arm/syscalls/tgkill.S:9 #5 0xef5ab511 in abort bionic/libc/bionic/abort.cpp:42 (discriminator 2) #6 0xef73b0a9 in __sanitizer::Abort() external/compiler-rt/lib/sanitizer_common/sanitizer_posix_libcdep.cc:141 #7 0xef73f831 in __sanitizer::Die() external/compiler-rt/lib/sanitizer_common/sanitizer_termination.cc:59 #8 0xef72a117 in ~ScopedInErrorReport [asan_rtl] #9 0xef72b38f in __asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) [asan_rtl] #10 0xef72bd33 in __asan_report_load1 [asan_rtl] #11 0xaaaae937 in FuzzMe(unsigned char const*, unsigned int) tools/fuzzers/fuzz_me_fuzzer/fuzz_me_fuzzer.cpp:10 #12 0xaaaaead7 in LLVMFuzzerTestOneInput tools/fuzzers/fuzz_me_fuzzer/fuzz_me_fuzzer.cpp:15 #13 0xaaab8d5d in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned int) external/llvm/lib/Fuzzer/FuzzerLoop.cpp:515 #14 0xaaab8f3b in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned int) external/llvm/lib/Fuzzer/FuzzerLoop.cpp:469 #15 0xaaab9829 in fuzzer::Fuzzer::MutateAndTestOne() external/llvm/lib/Fuzzer/FuzzerLoop.cpp:701 #16 0xaaab9933 in fuzzer::Fuzzer::Loop() external/llvm/lib/Fuzzer/FuzzerLoop.cpp:734 #17 0xaaab48e5 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned int)) external/llvm/lib/Fuzzer/FuzzerDriver.cpp:524 #18 0xaaab306f in main external/llvm/lib/Fuzzer/FuzzerMain.cpp:20 #19 0xef5a8da1 in __libc_init bionic/libc/bionic/libc_init_dynamic.cpp:114 SUMMARY: AddressSanitizer: heap-buffer-overflow ... ==28219==ABORTING MS: 1 CrossOver-; base unit: 10cc0cb80aa760479e932609f700d8cbb5d54d37 0x46,0x55,0x5a, FUZ artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60 Base64: RlVa
In the example output, the crash was caused by fuzz_me_fuzzer.cpp
at line 10:
Data[3] == 'Z'; // :(
This is a straightforward out-of-bounds read if Data is of length 3.
After you run your fuzzer, the output often results in a crash and the offending
input is saved in the corpus and given an ID. In the example output, this is
crash-0eb8e4ed029b774d80f2b66408203801cb982a60
.
To retrieve crash information, issue this command, specifying your crash ID:
adb pull /data/tmp/fuzz_me_fuzzer/corpus/CRASH_ID
For more information about libFuzzer, see the upstream documentation. Because Android's libFuzzer is a few versions behind upstream, check external/llvm/lib/Fuzzer to make sure the interfaces support what you're trying to do.