【开源软件供应链点亮计划】LibFuzzer学习

cascades2021-08-14Fuzzing鹏城实验室summer2021

LibFuzzer 学习

本文章来自于开源软件供应链点亮计划的 openEuler 社区项目 项目名称:No.112 qemu 设备 fuzz 测试完善

前言

LibFuzzer 是一个in-processcoverage-basedevolutionary的模糊测试引擎,是 LLVM 项目的一部分。它与被测库链接,通过特定的入口点将模糊测试的输入提供给被测函数。在测试过程中不断变异输入,并统计代码覆盖率和崩溃情况。

LibFuzzer 使用方法

实验环境

采用了鹏城实验室的云主机,操作系统为 openEuler

[root@host-10-0-0-94 libFuzzer]# lscpu
Architecture:                    aarch64
CPU op-mode(s):                  64-bit
Byte Order:                      Little Endian
CPU(s):                          4

[root@host-10-0-0-94 libFuzzer]# cat /etc/os-release
NAME="openEuler"
VERSION="20.03 (LTS-SP1)"

简单使用

入门教学

  1. 安装 llvm 和 clang

    • 源码编译:对于机器性能尤其是内存(8GB)和硬盘(15-20GB)的要求比较高,需要对编译命令进行一些优化。需要额外安装 LibFuzzer 依赖的compile-rt
    git clone https://gitee.com/mirrors/LLVM.git
    cd LLVM ; mkdir build ; cd build
    cmake -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;compiler-rt" -DCMAKE_BUILD_TYPE="Release" -DLLVM_TARGETS_TO_BUILD="host" -G "Unix Makefiles" ../llvm
    make -j4
    
    • 二进制安装:下载对应版本的二进制文件,方便在不同版本间切换。可以使用软链接添加到环境变量中方便使用。
    • 包管理器安装,版本较低,自带了 libFuzzer。
    # sudo apt/dnf search xxx 可以查看包管理器中包含的软件以及对应版本
    sudo apt/dnf install clang llvm compiler-rt
    
  2. 编译被测二进制文件,加入 LibFuzzer 的编译选项

    // LibFuzzer提供的函数接口,在被测源代码fuzz_me.cc中实现
    extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
      DoSomethingInterestingWithMyAPI(Data, Size);
      return 0;  // Non-zero return values are reserved for future use.
    }
    
    # clang会去寻找libclang_rt.xxx.a的静态链接库,即sanitizers
    # 除了ASAN,还可以添加UBSAN,TSAN等其他sanitizers
    clang -fsantize=address,fuzzer -g fuzz_me.c -o fuzz_me
    

如果使用该编译命令失败,说明上一步安装不成功,需要检查 compile-rt 库的位置或重新安装。

  1. 运行二进制文件,观察输出 bash ./fuzz_me grep ERROR ./*.log | sort -k 3 如果程序的异常行为被 sanitizer 检测到,会产生 Fuzzing output,即 crash。除了 crash,Fuzzer 还会记录 Fuzzing 过程中的参数,例如代码覆盖率(以基本块为单位),种子变异情况等等。

  2. 复现该输入,定位漏洞 bash ./fuzz_me crash-xxx ./fuzz_me -seed=xxx # xxx为crash的SHA-1哈希值 gdb fuzz_me sanitizer 给出了漏洞类型和漏洞触发的环境。可以重新观察 crash,复现漏洞,或放入 gdb 调试。

一些有用的 utils

面对大型软件时,还需要开启一些编译选项,来提高 Fuzzing 的效率:

  • -jobs:任务数,每个 job 的目的就是触发 crash,job 在 workers 进程中运行,一个 worker 可以管理多个 job。如果将 jobs 设置为 1000,可以绕过程序的简单 bug
  • -workers:进程数,最多可使用一半的 CPU 核心数
  • -forks:将-jobs = N-workers = N替换为-fork = N
  • -dict:字典,在 Fuzzing 特定格式的文件时非常必要
  • CORPUS:语料库,用于保存 Fuzzing 中触发新路径的输入
  • -max-len:设置最大输入长度,根据语料库文件大小来定义
  • -run:减少 crash producer,单位为每次迭代中的变异次数
  • -shrink:减小语料库大小,可能可以提高代码覆盖率

项目实践

LibFuzzer 原理

变异算法

变异(Mutation)是现代 Fuzzer 中的关键步骤,用于产生新的且能够覆盖更多基本块的输入。LibFuzzer 包含了一系列内置的简单的变异算法,大多为 bit 级反转。LibFuzzer 同时也接受用户自定义变异算法,用于定向 Fuzzing。

已有变异算法

通过观察 LibFuzzer 的Stderr Output,可以在 MS 字段发现当前输入使用的变异算法,如图所示。

LibFuzzer 一共内置了 12 种变异算法,属于MutationDispatcher类的成员函数,类定义代码如下:

// 代码路径:LLVM/compiler-rt/lib/fuzzer/FuzzerMutate.cpp

MutationDispatcher::MutationDispatcher(Random &Rand, const FuzzingOptions &Options) : Rand(Rand), Options(Options) {
    DefaultMutators.insert(
        DefaultMutators.begin(),
        {
            {&MutationDispatcher::Mutate_EraseBytes, "EraseBytes"},
            {&MutationDispatcher::Mutate_InsertByte, "InsertByte"},
            {&MutationDispatcher::Mutate_InsertRepeatedBytes, "InsertRepeatedBytes"},
            {&MutationDispatcher::Mutate_ChangeByte, "ChangeByte"},
            {&MutationDispatcher::Mutate_ChangeBit, "ChangeBit"},
            {&MutationDispatcher::Mutate_ShuffleBytes, "ShuffleBytes"},
            {&MutationDispatcher::Mutate_ChangeASCIIInteger, "ChangeASCIIInt"},
            {&MutationDispatcher::Mutate_ChangeBinaryInteger, "ChangeBinInt"},
            {&MutationDispatcher::Mutate_CopyPart, "CopyPart"},
            {&MutationDispatcher::Mutate_CrossOver, "CrossOver"},
            {&MutationDispatcher::Mutate_AddWordFromManualDictionary, "ManualDict"},
            {&MutationDispatcher::Mutate_AddWordFromPersistentAutoDictionary, "PersAutoDict"},
        });
        // 以上函数的具体实现
    }

大部分变异算法从名称就可以看出实现方法。比如EraseBytes即调用memmove函数覆盖掉部分比特位;InsertBytes即调用memmove函数添加一个比特位。值得注意的是,在这些内置的变异算法中,变异的位置的和变异的值都是采用Rand系列的随机函数生成。

如何添加新的变异算法

LibFuzzer 和 AFL 属于coverage-guided的 Fuzzing 工具,在 Fuzz 具体的对象时,可能由于变异算法不包含语义信息而导致在程序运行的初期就被过滤掉,相对于 generation-based 的 Fuzzing 工具效率低下。Google 由此提出了structure-aware-fuzzing的高级概念,可以让用户自行添加变异算法,即LibFuzzer Plugin,文中也介绍了如何添加 Plugin,并列举了一系列官方实现的 Plugin。

添加 Plugin 的方法如下面代码所示。首先需要实现一个自定义的LLVMFuzzerCustomMutator函数,加入特定的变异算法,然后在该函数中调用LLVMFuzzerMutate来实现普通的变异。

在具体的代码实现中,可以通过条件编译指令ifdef CUSTOM_MUTATORclang -DCUSTOM_MUTATOR来开启或关闭自定义的 Plugin。

// Optional user-provided custom mutator.
// Mutates raw data in [Data, Data+Size) inplace.
// Returns the new size, which is not greater than MaxSize.
// Given the same Seed produces the same mutation.
size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed);

// libFuzzer-provided function to be used inside LLVMFuzzerCustomMutator.
// Mutates raw data in [Data, Data+Size) inplace.
// Returns the new size, which is not greater than MaxSize.
size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize);

对于添加自己的 Plugin 感兴趣的同学,可以参考该文章中给出的参考链接,也可以参考 2017 年 LLVM 开发者大会上的议题"Structure-aware fuzzing for Clang and LLVM with libprotobuf-mutator"

覆盖率统计

代码覆盖率统计方法需要从两个维度分析:

  • 统计精度:从粗糙到精细,代码覆盖率(Coverage)的统计大致分为三个层次
    • function level:统计调用的函数,忽略对函数内部代码的统计
    • basic block level:统计标准块,可以在 LibFuzzer 的Stderr Output的 cov 字段查找到
    • edge level:edge 不仅包含了基本块信息,还在基本块之间建立虚拟块,以包含执行信息

  • 分析对象:统计的基本方法是插桩(instrumentation),加入计数变量,大致分为三个级别
    • source code:在编译选项中提供覆盖率统计方式
    • intermediate representation:用llvm pass等方式统计
    • binary:使用PinDynamoRIO等二进制插桩工具去 hook 统计

综上所述,LibFuzzer 使用了 LLVM 框架中的SanitizerCoverage来实现源代码级别的覆盖率统计,可以通过如下命令指定,默认使用edge级别。

# xxx=edge,bb,func,trace-pc-guard,inline-8bit-counters,inline-bool-flag,pc-table,trace-pc
clang -fsanitize-coverage=xxx fuzz_me.c

此外,还可以在SanitizerCoverage的基础上开发分析工具。Sanitizer 提供了覆盖率的回调接口,用户可以在 Fuzzing 进程关闭时将覆盖率统计的结果转储为.sancov文件。LLVM 框架提供了Sancov Tool工具来生成源代码级别的覆盖率报告。

错误检查

常见错误

前文提到,LibFuzzer 进程中的每个job的使命就是完成检查任务。它直到遇到 crash 或者运行超时后才会停止。这时,LibFuzzer 的守护进程会捕获错误码。如果错误码为 77,则为超时(默认超时时间为 1200 秒)或者 LibFuzzer 本身程序异常;如果为 crash,则将 crash 结果以及造成 crash 的输入记录下来。

Sanitizers

而仅仅检测出 crash 并不够覆盖所有 Fuzzing 的检测场景,比如内存泄漏,竞态等情况虽然可能不会造成 crash,但也是非常严重的一类错误。针对这些非 crash 的错误,有像Valgrind这样的重量级内存检测工具。而 LibFuzzer 使用的是 LLVM 框架中的一系列Sanitizers。这些工具由 Google 提出,可以用于检测 C/C++语言的各类运行时异常,比较常用的 Sanitizers 列举如下:

  • AddressSanitizer(ASAN):捕获堆栈溢出,UAF 等漏洞
  • ThreadSanitizer(TSAN):捕获 data race,支持 C/C++和 Go
  • UndefinedBehaviorSanitizer(UBSAN):捕获整数溢出,空指针解引用等异常行为

以 ASAN 为例来分析这些错误检查工具的原理,详细介绍可以看USENIX ATC 2021

  1. 在编译时,ASAN 在 LLVM IR 级别的访问内存操作(load,store,alloca)的前后插桩。由于内存有 8 字节对齐的要求,所以部分内存处于 unused 状态,可以使用内存映射的方式将其设置为shadow memory,来指示其读写情况。
  2. 在运行时对 malloc 函数进行 HOOK,并在前后设置Redzone区域,类似于 Stack Canary 的做法。将 Redzone 区域的 shadow memory 设置为不可写,即可避免溢出问题。
  3. 在运行时对 free 函数进行 HOOK,不立即释放该内存,而是将 shadow memory 设置为负数,即不可读写的状态,并放入隔离区观察。如果发生 UAF 或者野指针解引用的情况,则会被捕获。

其他 Fuzzer

  • 学术界:Fuzzing 作为学术热点,近年来在系统安全,网络安全,软件分析,程序语言等领域的国际顶级会议上都有许多相关论文,可以查看FuzzingPaper项目。这些论文大多是提出一种针对某一特定对象(软件,OS 内核,硬件,程序语言等等)或者特定漏洞类型(竞态条件,缓冲区溢出)的 Fuzzer,并结合符号执行(Concolic Fuzzing),深度学习等方法提高 Fuzzing 效率。

  • 工业界:在 AFL,LibFuzzer 这样的工具诞生之后,Github 上陆续有许多在此基础上开发的工具,也有许多主打模糊测试技术的公司兴起。其中比较著名的是 Google 提出的一系列 Fuzzing 工具,包括其维护的Fuzzbench平台,可以对 Fuzzer 性能进行统一评估。

总体来说,学术界和工业界在 Fuzzing 的研究上还是非常相关的。因为大部分 Fuzzing 技术基于 LLVM 框架实现,所以可扩展性很强。学术界的工作基于工业界的已有的工具扩展开发,而学术界性能较好的成果,也会发布到 Github 上。以下列举一些我在学习 Fuzzing 过程中收藏的 Fuzzer,更完整的版本可以查看Awesome-Fuzzing

有错误之处请批评指正,作者联系方式:cascades-sjtu


【免责声明】本文仅代表作者本人观点,与本网站无关。本网站对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文仅供读者参考,由此产生的所有法律责任均由读者本人承担。