使用 LibFuzzer 时,编译 fuzz target 是需要使用者自己执行的重要步骤。在编译时,通过编译选项添加消毒器,链接 LibFuzzer 静态库,得到可执行的 fuzz target。

本文针对 LibFuzzer Target 编译进行说明。

一、使用开源脚本

fuzzer-test-suite 是 Google 开源的一个 Fuzz 基准测试集,包含 Fuzz(LibFuzzer 和 AFL)测试的多个 benchmark。这些 benchmark 都来自真实案例,涉及到图像处理、SQL 注入、JSON 解析等多个领域。

c-ares-CVE 为例,编译主要用到以下三个 sh 脚本,以及实现了 LLVMFuzzerTestOneInput 的 cc 源码文件:

1
2
3
4
5
6
fuzzer-test-suite
│ common.sh
│ custom-build.sh
├─c-ares-CVE-2016-5180
│ build.sh
│ target.cc

1. 编译过程

编译入口为 build.sh 文件,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# Copyright 2016 Google Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
. $(dirname $0)/../custom-build.sh $1 $2
. $(dirname $0)/../common.sh

build_lib() {
rm -rf BUILD
cp -rf SRC BUILD
(cd BUILD && ./buildconf && ./configure --disable-shared && make -j $JOBS)
}
get_git_revision https://github.com/c-ares/c-ares.git 51fbb479f7948fca2ace3ff34a15ff27e796afdd SRC
build_lib
build_fuzzer

if [[$FUZZING_ENGINE == "hooks"]]; then
# Link ASan runtime so we can hook memcmp et al.
LIB_FUZZING_ENGINE="$LIB_FUZZING_ENGINE -fsanitize=address"
fi
$CXX $CXXFLAGS $SCRIPT_DIR/target.cc -I BUILD BUILD/.libs/libcares.a $LIB_FUZZING_ENGINE -o $EXECUTABLE_NAME_BASE

执行 build.sh 时,首先依次调用外层的 custom-build.shcommon.sh,然后执行如下步骤:

  1. get_git_revision:定义在 common.sh 中,拉取有漏洞的代码

  2. build_lib:将拉取的代码编译成静态库文件 libcares.a,注意--disable-shared 参数指定编译静态库文件,否则编译动态库文件

  3. build_fuzzer:定义在 common.sh 中,编译 Fuzz 引擎源码(如果使用 LibFuzzer,则编译 LibFuzzer 源码,得到libFuzzer.a

  4. \$CXX \$CXXFLAGS \$SCRIPT_DIR/target.cc:编译 target.cc 文件,链接 libcares.alibFuzzer.a,得到可执行的 fuzz target

在整个编译过程中,需要重点关注的是 编译选项的设置 静态库的链接

2. 编译选项

build_lib中编译待测源码时,使用的是 configure-make 工具,该工具广泛用于 Linux 环境中编写的 C/C++ 程序的编译(近年来开始使用 cmake 工具),主要包含两个步骤:

  • configure对编译过程进行配置,通过参数和环境变量控制编译,生成 makefile
  • make从 makefile 中读取命令并执行编译,生成二进制文件

通过 ./configure -h 可以查看编译参数和环境变量的含义,脚本中用到的参数和环境变量列举如下:

参数 含义
–enable-shared 编译动态库文件(默认 yes)
–enable-static 编译静态库文件(默认 yes)
–disable-shared 不编译动态库文件
–disable-static 不编译静态库文件
环境变量 含义
CC C 编译程序
CXX C++ 编译程序
CFLAGS C 编译程序的命令行参数
CXXFLAGS C++ 编译程序的命令行参数

在执行 build_lib 时,设置了 --disable-shared 参数,指定将待测源码编译成静态库文件。在 custom-build.shcommon.sh中,设置了如下环境变量:

  1. custom-build.sh

    • asan 模式

      1
      2
      3
      export FUZZING_ENGINE=libfuzzer
      export CFLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address -fsanitize-address-use-after-scope -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div"
      export CXXFLAGS="${CFLAGS}"
    • ubsan 模式

      1
      2
      3
      export FUZZING_ENGINE=libfuzzer
      export CFLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=undefined -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div"
      export CXXFLAGS="${CFLAGS}"
    • 默认模式

      1
      2
      FUZZING_ENGINE=${FUZZING_ENGINE:-"fsanitize_fuzzer"}
      FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address -fsanitize-address-use-after-scope -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div"
  2. common.sh

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address -fsanitize-address-use-after-scope -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div"
    ......
    export CC=${CC:-"clang"}
    export CXX=${CXX:-"clang++"}
    export CPPFLAGS=${CPPFLAGS:-"-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION"}
    export LIB_FUZZING_ENGINE="libFuzzingEngine-${FUZZING_ENGINE}.a"

    if [[$FUZZING_ENGINE == "fsanitize_fuzzer"]]; then
    FSANITIZE_FUZZER_FLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope"
    export CFLAGS=${CFLAGS:-$FSANITIZE_FUZZER_FLAGS}
    export CXXFLAGS=${CXXFLAGS:-$FSANITIZE_FUZZER_FLAGS}
    elif
    ......
    else
    export CFLAGS=${CFLAGS:-"$FUZZ_CXXFLAGS"}
    export CXXFLAGS=${CXXFLAGS:-"$FUZZ_CXXFLAGS"}
    fi

可以看到脚本指定了 CCCXX编译工具为 clangclang++,在 CFLAGSCXXFLAGS中设置了 clang 的编译选项:

选项 含义
-O2 指定编译器优化等级为 2
-fno-omit-frame-pointer 将 frame pointer 保存在寄存器中。frame pointer 即栈帧基地址(指向栈底),在编译器优化时或某些平台上,fp 会被忽略,通过此选项可强制开启
-gline-tables-only 函数调用栈中包含函数名、文件名和行号等信息,但不包括局部变量和函数参数。要包括全部信息应使用 -g 选项
-fsanitize=fuzzer 编译时对源码插桩,链接 libFuzzer 库文件
-fsanitize=fuzzer-no-link 编译时对源码插桩,不链接 libFuzzer 库文件。编译待测源码时不链接,编译 fuzz target 时再链接,对大型项目可以提升编译效率
-fsanitize=address 启用 AddressSanitizer,检测缓冲区溢出、UAF、Double Free、内存泄漏等内存问题
-fsanitize-address-use-after-scope 启用局部变量的 sanitization 以检测 use-after-scope 错误(作用域外访问)
-fsanitize=undefined 启用 UndefinedSanitizer,检测程序运行中越界访问、整数溢出、空指针解引用等未定义行为
-fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div 开启代码覆盖率统计。在高版本 clang 中 -fsanitize-coverage=trace-pc-guard 已弃用,默认开启代码覆盖率统计,使用此选项会报错

3. 链接库文件

编译 fuzz target 时,执行了如下命令:

1
$CXX $CXXFLAGS $SCRIPT_DIR/target.cc -I BUILD BUILD/.libs/libcares.a $LIB_FUZZING_ENGINE -o $EXECUTABLE_NAME_BASE

环境变量 CXXCXXFLAGS沿用之前定义,通过-I(uppercase i)选项,指定了.h 头文件目录和.a 静态库路径:

  • BUILD:存放待测源码的头文件

  • BUILD/.libs/libcares.a:编译待测源码生成的静态库文件libcares.a

  • $LIB_FUZZING_ENGINE:编译 LibFuzzer 源码生成的libFuzzer.a

链接库文件时,-I(uppercase i)可以用 -L-l(lowercase L)替换:

1
-I BUILD/.libs/libcares.a  等价于  -L BUILD/.libs -l cares

特别地,链接 libFuzzer.a 库文件时,可以用过 -fsanitize=fuzzer 实现:

1
-I libFuzzer.a 等价于 -fsanitize=fuzzer

二、自己编写命令

开源的编译脚本为了适配多个 benchmark 和多种 Fuzz 类型,做了较多逻辑处理,适配新的 benchmark 时不够灵活。通过对编译脚本的流程分析,很容易自己编写命令完成编译。

在 Linux 环境下安装 clang,以 c-ares-CVE 为例,编写命令生成 fuzz target:

0. 准备源码

1
2
3
git clone https://github.com/c-ares/c-ares.git
cd c-ares/
git reset --hard 51fbb479f7948fca2ace3ff34a15ff27e796afdd

此时查看目录内容如下:

1
2
3
4
5
6
root@server:~/fuzz/cares$ tree -L 1
.
├── c-ares
└── target.cc

1 directory, 1 file

1. 设置环境变量

设置CCCXXCFLAGSCXXFLAGS

1
2
3
4
export CC=clang
export CXX=clang++
export CFLAGS="-O2 -fno-omit-frame-pointer -gline-tables-only -fsanitize=address,fuzzer-no-link -fsanitize-address-use-after-scope"
export CXXFLAGS=$CFLAGS

2. 编译待测库

进入源码目录,执行configure 脚本,设置禁止编译动态库文件,执行 make 编译libcares.a

1
2
3
4
cd c-ares
./buildconf
./configure --disable-shared
make -j4

编译成功后,会在源码目录下生成.lib 目录,存放生成的 libcares.a 文件

1
2
3
root@server:~/fuzz/cares/c-ares$ cd .libs/
root@server:~/fuzz/cares/c-ares/.libs$ ls
libcares.a libcares.la libcares.lai

3. 编译 fuzz target

由于安装 clang 时已经自带 LibFuzzer.a,因此不用再编译LibFuzzer.a,回到target.cc 目录,直接编译 targe.cc 链接 libFuzzer 即可

1
2
cd ~/fuzz/cares
$CXX $CXXFLAGS target.cc -fsanitize=fuzzer -g -I c-ares c-ares/.libs/libcares.a -o cares-fuzzer

编译成功后,会在当前目录下生成可执行文件 cares-fuzzer,执行cares-fuzzer 开始 fuzz 测试,可以看到触发了 crash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
root@server:~/fuzz/cares$ ls
c-ares cares-fuzzer target.cc
root@server:~/fuzz/cares$ ./cares-fuzzer
INFO: Seed: 3031255060
INFO: Loaded 1 modules (10 inline 8-bit counters): 10 [0x5a90e0, 0x5a90ea),
INFO: Loaded 1 PC tables (10 PCs): 10 [0x56c278,0x56c318),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 27Mb
#4 NEW cov: 4 ft: 4 corp: 2/3b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 2 ChangeBit-InsertByte-
#1331 NEW cov: 6 ft: 6 corp: 3/20b lim: 17 exec/s: 0 rss: 27Mb L: 17/17 MS: 2 InsertByte-CrossOver-
#1368 REDUCE cov: 6 ft: 6 corp: 3/19b lim: 17 exec/s: 0 rss: 27Mb L: 16/16 MS: 2 CrossOver-EraseBytes-
#1048576 pulse cov: 6 ft: 6 corp: 3/19b lim: 4096 exec/s: 524288 rss: 773Mb
#2097152 pulse cov: 6 ft: 6 corp: 3/19b lim: 4096 exec/s: 419430 rss: 773Mb
=================================================================
==664326==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000d2c052 at pc 0x000000550e1c bp 0x7ffd914ddd30 sp 0x7ffd914ddd28
WRITE of size 1 at 0x603000d2c052 thread T0
#0 0x550e1b in ares_create_query (/home/rootfuzz/cares/cares-fuzzer+0x550e1b)
#1 0x55053c in LLVMFuzzerTestOneInput /home/rootfuzz/cares/target.cc:14:3
#2 0x4586a1 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/rootfuzz/cares/cares-fuzzer+0x4586a1)
#3 0x457de5 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) (/home/rootfuzz/cares/cares-fuzzer+0x457de5)
#4 0x45a087 in fuzzer::Fuzzer::MutateAndTestOne() (/home/rootfuzz/cares/cares-fuzzer+0x45a087)
#5 0x45ad85 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/home/rootfuzz/cares/cares-fuzzer+0x45ad85)
#6 0x44973e in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/rootfuzz/cares/cares-fuzzer+0x44973e)
#7 0x472582 in main (/home/rootfuzz/cares/cares-fuzzer+0x472582)
#8 0x7f4b400f8082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
#9 0x41e4dd in _start (/home/rootfuzz/cares/cares-fuzzer+0x41e4dd)

0x603000d2c052 is located 0 bytes to the right of 18-byte region [0x603000d2c040,0x603000d2c052)
allocated by thread T0 here:
#0 0x51e20d in malloc (/home/rootfuzz/cares/cares-fuzzer+0x51e20d)
#1 0x5508f6 in ares_create_query (/home/rootfuzz/cares/cares-fuzzer+0x5508f6)
#2 0x55053c in LLVMFuzzerTestOneInput /home/rootfuzz/cares/target.cc:14:3
#3 0x4586a1 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/rootfuzz/cares/cares-fuzzer+0x4586a1)
#4 0x457de5 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) (/home/rootfuzz/cares/cares-fuzzer+0x457de5)
#5 0x45a087 in fuzzer::Fuzzer::MutateAndTestOne() (/home/rootfuzz/cares/cares-fuzzer+0x45a087)
#6 0x45ad85 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/home/rootfuzz/cares/cares-fuzzer+0x45ad85)
#7 0x44973e in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/rootfuzz/cares/cares-fuzzer+0x44973e)
#8 0x472582 in main (/home/rootfuzz/cares/cares-fuzzer+0x472582)
#9 0x7f4b400f8082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/rootfuzz/cares/cares-fuzzer+0x550e1b) in ares_create_query
Shadow bytes around the buggy address:
0x0c068019d7b0: fd fa fa fa fd fd fd fa fa fa fd fd fd fa fa fa
0x0c068019d7c0: fd fd fd fa fa fa fd fd fd fa fa fa fd fd fd fa
0x0c068019d7d0: fa fa fa fa fa fa fa fa fd fd fd fa fa fa fd fd
0x0c068019d7e0: fd fa fa fa fd fd fd fa fa fa fd fd fd fa fa fa
0x0c068019d7f0: fa fa fa fa fa fa fa fa fa fa fa fa fd fd fd fa
=>0x0c068019d800: fa fa fa fa fa fa fa fa 00 00[02]fa fa fa fa fa
0x0c068019d810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c068019d820: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c068019d830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c068019d840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c068019d850: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==664326==ABORTING
MS: 4 ChangeBit-InsertByte-EraseBytes-ChangeByte-; base unit: 7c9981f8ba526a6e0b8c1bbb846c08e2d6026e3a
0x5c,0x2e,
\\.
artifact_prefix='./'; Test unit written to ./crash-c9257f8fd31ea852baf734ef06d37348bf6e8cb2
Base64: XC4=

三、补充说明

1. 脚本执行

mode参数执行 build.sh 脚本时,需要编译 LibFuzzer 源码,这时候会出现找不到 LibFuzzer 源码的错误。需要自己下载源码,并放到指定路径,调试脚本解决问题,比较麻烦。

建议不带参数执行build.sh,这时采用默认模式,编译时会采用 address 消毒器,这也是应用的最多的一种消毒器。

2. 静态库 or 动态库

编译待测库的源码时,一定要编译成静态库文件 ,在congfigure 时使用 --disable-shared 参数。

如果是编成动态库文件,或静态库动态库同时都有的情况下,fuzz target 会优先链接动态库文件。而动态库会导致 fuzz 测试失效。

第一个图中使用静态库,fuzz 很快能发现 crash,第二个图中是用动态库,fuzz 运行很慢,找不到 crash。

使用静态库得到 57 PCs,使用动态库得到 5 PCs,二者有明显区别,推测使用动态库可能会导致插桩失效,影响 fuzz 运行效率。