通过 libFuzzer 进行模糊测试

模糊测试(将可能无效的数据、异常数据或随机数据作为输入内容提供给程序)是在大型软件系统中查找错误的一种非常有效的方式,也是软件开发生命周期的重要组成部分。

Android 编译系统通过从 LLVM 编译器基础架构项目纳入 libFuzzer 项目来支持模糊测试。LibFuzzer 会与被测函数相关联,并会处理在模糊测试会话期间出现的所有输入选择、变更和崩溃报告。LLVM 的排错程序用于协助内存损坏检测以及提供代码覆盖率指标。

本文介绍了 Android 上的 libFuzzer 以及如何执行插桩编译,还介绍了如何编写、运行和自定义模糊测试工具。

设置和编译

为了确保映像能够在设备上正常运行,请按照以下设置和编译示例进行操作。

向您的设备中刷入标准 Android 编译版本后,请按照说明刷入 AddressSanitizer 编译版本,然后使用 SANITIZE_TARGET='address coverage'(而非 SANITIZE_TARGET='address')开启覆盖率指标。

设置示例

本例假设目标设备为 Pixel (sailfish),且已为 USB 调试 (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

编译示例

要创建支持可重现模糊测试会话的插桩系统映像,您需要完成具有两个步骤的编译过程。

首先,请执行完整的 Android 编译过程,并将编译得到的版本刷入设备。接下来,以现有编译版本为起点,编译 Android 的插桩版本。编译系统会根据情况只编译所需的二进制文件并将其放在正确的位置。

  1. 通过执行以下命令来执行初始编译:
    make -j$(nproc)
  2. 要刷写设备,请在启动设备后使用相应的键组合进入 fastboot 模式。
  3. 解锁引导加载程序,并使用下列命令刷入新编译的映像(-w 选项可用于擦除用户数据,以确保干净的初始状态)。
    fastboot oem unlock
    fastboot flashall -w
    
  4. 执行插桩编译,并将修改后的二进制文件刷入设备:
    make -j$(nproc) SANITIZE_TARGET='address coverage'
    fastboot flash userdata
    fastboot flashall

目标设备现在应该已经准备好进行 libFuzzer 模糊测试。为了确保您的编译是插桩编译,请将 adb 作为根来检查 /data/asan/lib 是否存在:

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

常规的非插桩编译中不存在这类目录。

编写模糊测试工具

为了说明如何在 Android 中使用 libFuzzer 编写端到端的模糊测试工具,请将以下易受攻击的代码作为测试用例。这样做有助于对模糊测试工具进行测试,确保一切运行正常,并说明崩溃数据是什么样的。

以下是测试函数。

#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
}

要编译并运行此模糊测试工具,请执行以下操作:

  1. 在 Android 源代码树中创建一个目录,例如,tools/fuzzers/fuzz_me_fuzzer。后续文件都将在此目录下创建。
  2. 使用 libFuzzer 编写模糊测试目标。模糊测试目标是一个函数,该函数可接收指定大小的 blob 数据,并将其传递给要接受模糊测试的函数。以下是针对易受攻击的测试函数的基本模糊测试工具:
    extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) {
      FuzzMe(buf, len);
      return 0;
    }
    
  3. 指示 Android 的编译系统创建模糊测试工具二进制文件。 要编译模糊测试工具,请将此代码添加到 Android.mk 文件:
    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)
    

    实现这个目的所需的大部分逻辑都包含在 BUILD_FUZZ_TEST 宏(在 build/core/fuzz_test.mk. 中进行定义)中

  4. 使用以下代码编译模糊测试工具:
    make -j$(nproc) fuzz_me_fuzzer SANITIZE_TARGET="address coverage"
    

完成这些步骤之后,您便会得到一个编译好的模糊测试工具。模糊测试工具的默认位置(本例中为 Pixel 版本)为 out/target/product/sailfish/data/nativetest/fuzzers/fuzz_me_fuzzer/fuzz_me_fuzzer

运行您的模糊测试工具

编译好模糊测试工具之后,请上传该工具和易受攻击的库以进行关联。

  1. 要将这些文件上传到设备上的某个目录下,请运行以下命令:
    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. 使用以下命令运行模糊测试工具:
    adb shell /data/tmp/fuzz_me_fuzzer/fuzz_me_fuzzer /data/tmp/fuzz_me_fuzzer/corpus

执行此操作后,系统将输出类似于下方示例的内容。

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

在示例输出中,崩溃是由第 10 行中的 fuzz_me_fuzzer.cpp 导致的:

      Data[3] == 'Z';  // :(

如果数据长度为 3,会导致出界读取错误。

运行模糊测试工具后,输出常常会导致崩溃,而导致问题的输入则会保存到语料库中,并被指定一个 ID。在本示例输出中,ID 为 crash-0eb8e4ed029b774d80f2b66408203801cb982a60

要检索崩溃信息,请运行以下命令(先指定好您的崩溃 ID):

adb pull
/data/tmp/fuzz_me_fuzzer/corpus/CRASH_ID

要详细了解 libFuzzer,请参阅上游文档。由于 Android 的 libFuzzer 比上游低了几个版本,因此请检查 external/llvm/lib/Fuzzer,以确保接口支持您正尝试执行的操作。