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.

Setup and build

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'.

Setup example

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

Build example

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.

  1. Perform the initial build by issuing:
    make -j$(nproc)
  2. To allow you to flash your device, boot your device into fastboot mode using the appropriate key combination.
  3. Unlock the bootloader and flash the newly compiled image with the following commands. (The -w option erases userdata, ensuring a clean initial state.)
    fastboot oem unlock
    fastboot flashall -w
    
  4. Perform the instrumented build and flash the modified binaries to the device:
    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.

Write a fuzzer

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:

  1. Create a directory in the Android source tree, for example, tools/fuzzers/fuzz_me_fuzzer. The following files will all be created in this directory.
  2. Write a fuzz target using libFuzzer. The fuzz target is a function that takes a blob of data of a specified size and passes it to the function to be fuzzed. Here's a basic fuzzer for the vulnerable test function:
    extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) {
      FuzzMe(buf, len);
      return 0;
    }
    
  3. Tell Android's build system to create the fuzzer binary. To build the fuzzer, add this code to the 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.

  4. Make the fuzzer with:
    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

Run your fuzzer

After you've built your fuzzer, upload the fuzzer and the vulnerable library to link against.

  1. To upload these files to a directory on the device, run these commands:
    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/
     
  2. Run the test fuzzer with this command:
    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.