将libfuzzer移植为动态链接库的实践:增强灵活性与应用场景拓展插图

在llvm中,libfuzzer是llvm-project下的compiler-rt的一部分。通常情况下,我们将libfuzzer静态链接到可执行文件中。本文将介绍一种将libfuzzer移植为动态链接库的实践方法。

将libfuzzer移植为动态链接库的实践:增强灵活性与应用场景拓展插图1

背景知识介绍

下面是一个官方的示例代码,非常简单易懂:

#include <stdint.h>
#include <stddef.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size > 0 && data[0] == 'H')
    if (size > 1 && data[1] == 'I')
       if (size > 2 && data[2] == '!')
       __builtin_trap();
  return 0;
}

编译命令如下:

clang++ -fsanitize=address,fuzzer test_fuzzer.cc

这里的address用于检测内存破坏,与libfuzzer本身无关。真正起作用的是-fsanitize=fuzzer

通过添加-v参数打印编译细节,如果添加了-fsanitize=fuzzer,就会在编译期进行插桩,并在连接期将其与/usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/libclang_rt.fuzzer-x86_64.a进行连接。而main函数是由libclang_rt.fuzzer-x86_64.a提供的。

如果不想使用libfuzzer提供的main函数,需要使用-fsanitize=fuzzer-no-link,表示只进行编译期插桩,并且需要将其与/usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a进行连接。开发者需要提供自己的main函数,并在适当的时机调用约定好的LLVMFuzzerRunDriver函数。

下面是一个简单的示例代码:

// clang++ -fsanitize=fuzzer-no-link test_fuzzer.cc /usr/lib/llvm-14/lib/clang/15.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a
#include <stdint.h>
#include <stddef.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size > 0 && data[0] == 'H')
    if (size > 1 && data[1] == 'I')
       if (size > 2 && data[2] == '!')
       __builtin_trap();
  return 0;
}

extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv, int (*UserCb)(const uint8_t *Data, size_t Size));

int main(int argc, char** argv){
    return LLVMFuzzerRunDriver(&argc, &argv, LLVMFuzzerTestOneInput);
}

无论使用哪种方式,最终都要进行静态链接的步骤。但并不是所有的二进制文件都能将libfuzzer静态链接进去,因此本文提出了一个实验性的方案。

思路

目标:获取与libclang_rt.fuzzer_no_main-x86_64.a对应的动态链接库libclang_rt.fuzzer_no_main-x86_64.so,并成功使用它。

llvm本身是由cmake组织的,虽然compiler-rt是它的子项目,但compiler-rt的功能是在运行时实现的,比如asan功能、libfuzzer功能。而cmake通过STATIC和SHARED来区分静态库和动态库。我们的初步想法是简单地将STATIC改为SHARED,看看运气如何。

阅读官方文档https://compiler-rt.llvm.org/后,编译命令如下:

cd llvm-project
mkdir build-compiler-rt
cd build-compiler-rt
cmake ../compiler-rt -DLLVM_CONFIG_PATH=/path/to/llvm-config
make

阅读https://github.com/llvm/llvm-project/tree/main/compiler-rt后,cmake也很好理解。我们从CMakeLists.txt和lib/fuzzer/CMakeLists.txt入手。

另外,由于https://github.com/llvm/llvm-project/commit/50a1c697127749eec567d14819d549b63af1242f,在llvm版本小于等于8时,接受trace-c、trace-pc-guard插桩和inline-8bit-counters插桩,而在llvm版本大于等于9时,只支持inline-8bit-counters插桩。

在许多情况下,被测目标不能在指定的clang下进行编译,因此我们需要验证libfuzzer在这两种情况下的表现。

compiler-rt 8.0

第一步:正常编译通过

只编译相关内容:

cmake /path/to/compiler-rt-8.0.0 -DCOMPILER_RT_BUILD_BUILTINS=OFF -DCOMPILER_RT_BUILD_SANITIZERS=OFF -DCOMPILER_RT_BUILD_XRAY=OFF -DCOMPILER RT_BUILD_LIBFUZZER=ON -DCOMPILER_RT_BUILD_PROFILE=OFF
make libfuzzer

编译参数的含义:

编译参数的含义:

- `-DCOMPILER_RT_BUILD_BUILTINS=OFF`:不编译builtins(该选项用于asan功能,与libfuzzer无关)
- `-DCOMPILER_RT_BUILD_SANITIZERS=OFF`:不编译sanitizers(该选项用于asan功能,与libfuzzer无关)
- `-DCOMPILER_RT_BUILD_XRAY=OFF`:不编译xray(与libfuzzer无关)
- `-DCOMPILER_RT_BUILD_LIBFUZZER=ON`:编译libfuzzer
- `-DCOMPILER_RT_BUILD_PROFILE=OFF`:不编译profile符号表(与libfuzzer无关)

生成的libclang_rt.fuzzer_no_main-x86_64.so在`build-compiler-rt/lib/clang/8.0.0/lib/linux`目录下。

将其复制到当前目录下,并用来编译示例代码:
cp build-compiler-rt/lib/clang/8.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.so .
clang++ -fsanitize=fuzzer-no-link test_fuzzer.cc ./libclang_rt.fuzzer_no_main-x86_64.so

运行编译后的可执行文件,可以看到正常输出。

第二步:修改CMakeLists.txt

lib/fuzzer/CMakeLists.txt中的add_compiler_rt_component(libfuzzer)改为add_compiler_rt_component(libfuzzer SHARED)

重新编译:

make libfuzzer

生成的libclang_rt.fuzzer_no_main-x86_64.so在build-compiler-rt/lib/clang/8.0.0/lib/linux目录下。

将其复制到当前目录下,并用来编译示例代码:

cp build-compiler-rt/lib/clang/8.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.so .
clang++ -fsanitize=fuzzer-no-link test_fuzzer.cc ./libclang_rt.fuzzer_no_main-x86_64.so

运行编译后的可执行文件,可以看到正常输出。

第三步:修改libfuzzer示例代码

将示例代码中的extern "C" int LLVMFuzzerRunDriver改为extern "C" __attribute__((visibility("default"))) int LLVMFuzzerRunDriver

重新编译示例代码:

clang++ -fsanitize=fuzzer-no-link -shared -fPIC test_fuzzer.cc -o test_fuzzer.so ./libclang_rt.fuzzer_no_main-x86_64.so

运行编译后的动态链接库,可以看到正常输出。

compiler-rt 9.0及以上版本

与compiler-rt 8.0的操作类似,只是在cmake命令中使用了不同的路径:

cmake /path/to/compiler-rt-9.0.0 -DCOMPILER_RT_BUILD_BUILTINS=OFF -DCOMPILER_RT_BUILD_SANITIZERS=OFF -DCOMPILER_RT_BUILD_XRAY=OFF -DCOMPILER_RT_BUILD_LIBFUZZER=ON -DCOMPILER_RT_BUILD_PROFILE=OFF
make libfuzzer

其他步骤与compiler-rt 8.0相同。

结论

通过将libfuzzer移植为动态链接库的实践,我们可以在使用libfuzzer的同时,避免了静态链接到二进制文件中的限制。这种方式可以提供更大的灵活性,并且在一些特定情况下可能会有所帮助。需要注意的是,移植过程可能因llvm版本的不同而有差异,需要根据具体情况进行调整。