KendyZ

KendyZ

A good idealistic young man
twitter
github
bilibili

记一次 Fuzzing tcpdump + DFSan 问题解决过程

起因#

事情是这样的。

我想要复现 NestFuzz 这篇论文的结果,于是找到了作者给出的 Github 仓库:fdu-sec/NestFuzz。对于这篇论文在实验部分评估的可能存在漏洞的程序,我最感兴趣的是 tcpdump,而仓库 README 中给出的是 tiff,虽然配置和运行过程应该是大差不差的,但是可以预想到确实可能存在一些细微的差别。

根据 README 的指示,首先编译 NestFuzz 的 fuzzer:

cd NestFuzz
make

此时,在 NestFuzz 目录下已经产生了编译后的 afl-fuzz 程序。接着需要额外编译 NestFuzz 的 modeling 组件,位于 ipl-modeling 目录下。按照目录下的另一个 README,配置 llvm-10 以及 Rust 工具链,进行编译。

  1. 构建 llvm-10
apt-get install -y xz-utils cmake ninja-build gcc g++ python3 doxygen python3-distutils
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/llvm-project-10.0.0.tar.xz
tar xf llvm-project-10.0.0.tar.xz
mkdir llvm-10.0.0-install
cd llvm-project-10.0.0
mkdir build
cd build
CC=gcc CXX=g++ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;libcxx;libcxxabi;lldb;compiler-rt" -DCMAKE_INSTALL_PREFIX=/path/to/llvm-10.0.0-install -DCMAKE_EXE_LINKER_FLAGS="-lstdc++" ../llvm
ninja install
# install rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

# other dependencies
apt install git zlib1g-dev python-is-python3 -y

# llvm
export LLVM_HOME=/path/to/llvm-10.0.0-install
export PATH=$LLVM_HOME/bin:$PATH
export LD_LIBRARY_PATH=$LLVM_HOME/lib:$LD_LIBRARY_PATH

./build.sh

然后就需要编译构建 target 了,从 the-tcpdump-group/tcpdump 将 master 上的最新版本 clone 下来,分别构建用于 fuzzing 和用于 modeling 的可执行程序。

  1. 构建用于 fuzzing 的 target
cp -r tcpdump tcpdump-fuzzer
cd tcpdump-fuzzer
./autogen.sh
CC=/path/to/NestFuzz/afl-gcc CXX=/path/to/NestFuzz/afl-g++ ./configure
make -j$(nproc)
  1. 接着构建用于 modeling 的 target。和上面一步不同是,这一步所用的 CC 和 CXX 编译器需要用到之前生成的 modeling 目录下的 test-clang 和 test-clang++ 程序,这两个程序对 clang 做了一层包装,插入了额外的一些参数,通过传入 DFSan 的 pass 来实现动态数据流分析。

image.png

问题出现#

从这里开始,事情向着和 README 给出的步骤不一样的方向发展了。

cp -r tcpdump tcpdump-model
cd tcpdump-model
./autogen.sh
CC=/path/to/NestFuzz/ipl-modeling/install/test-clang CXX=/path/to/NestFuzz/ipl-modeling/install/test-clang++ ./configure
make -j$(nproc)

在 configure 时,会报错:

image.png

在 config.log 中的最后一个报错如下:

image.png

在 ld 链接的过程中,发现被使用到的符号 dfs$pcap_loop 不存在。我一开始以为 dfs 这个单词可能是 deep-first-search(深度优先)的意思,没有什么其他的特殊含义,即找不到的符号实际上就是 pcap_loop 变量或者函数。实际上 pcap_loopthe-tcpdump-group/libpcap 提供的一个函数。那么可能是 libpcap 这个包在链接时没有在电脑中找到。

我尝试用 apt 安装 libpcap-dev,装好了之后能够使用 pcap-config 生成编译选项,说明 libpcap 安装的没问题。但是 configure 依然报相同的错误。

我纠结了一整天时间,甚至尝试了将系统环境由 debian 更换为论文中使用的 ubuntu,依旧不行。直到我意识到 dfs 可能是 DFSan 的缩写,或者至少与 DFSan 是有关的。

clang 载入的两个 pass:libLoopHandlingPass 和 libDFSanPass,在 llvm-10 的目录下都能够找到源码,分别为 LoopHandlingPass.cpp 和 DataFlowSanitizer.cpp,一搜就能发现,是后者在 pcap_loop 函数名之前加上了 dfs$

image.png

DFSan 原理#

DFSan 在 LLVM 的 pass 层级上实现了动态数据流分析,通过在函数调用、运算符等位置改写代码来实现污点追踪。所谓的污点追踪(Taint Tracking),就是给指定的变量打上污点标签(taint label),当这个变量的值会随着程序执行的过程,被其他指令使用到,例如作为参数传入某个函数,被赋予某个变量,被加到某个数字上去时,函数的返回值或者收到影响的变量也会被打上标签。

DFSan 实现污点追踪的原理其实很简单(LLVM 介绍 DFSan 的文档),对于函数的传参和返回值,DFSan 会改写这个函数的签名,在参数列表的末尾加上每个参数对应的 label 参数,并且额外 return 一个对应于返回值 label 变量。

DFSan 允许开发者提供一个 ABI List,来指定对涉及到的函数的重写行为:

image.png

image.png

对于添加一个 dfs$ 前缀的原因,DFSan 也解释了:

注意:在 LLVM-10/11 中,采用了添加 dfs$ 前缀的方式,在最新的 LLVM-19 中,已经变成添加后缀 .dfsan 了。

image.png

链接器进行链接的时候只会匹配符号名,而不会像编译器进行函数重载那样 “认真” 的选择被调用的函数定义(当然有的编译器会将参数列表和返回值以缩写的方式包含在符号名里面),这样对于相同的函数名而言,就无法区分哪些是被 DFSan 处理过,哪些没有了。假设 DFSan 不改写符号名,程序也以被 DFSan 处理之后的观点去看待 apt 安装的 pcap_loop 函数,那必然会导致参数个数的不匹配。

也就是说,在 clang 编译器编译了 tcpdump 的源码为 LLVM IR,然后经过 DataFlowSanitizer 处理之后,源码中对 pcap_loop 函数的引用都已经被改写为了 dfs$pcap_loop,而通过 apt 安装的 libpcap 库中只有 pcap_loop 符号,而没有 dfs$pcap_loop,因为这个 libpcap 在编译时并没有用 DFSan 处理过。

其实也可以模拟 configure 脚本的行为,将这个过程模拟一遍。configure 脚本会生成一个较小的源文件,名字叫 conftest.c,通过编译链接来判断某个函数是否存在于某个库中。在检查 libpcap 中的 pcap_loop 函数时,conftest.c 的内容为:

/* confdefs.h */
#define PACKAGE_NAME "tcpdump"
#define PACKAGE_TARNAME "tcpdump"
#define PACKAGE_VERSION "5.0.0-PRE-GIT"
#define PACKAGE_STRING "tcpdump 5.0.0-PRE-GIT"
#define PACKAGE_BUGREPORT "https://github.com/the-tcpdump-group/tcpdump/issues"
#define PACKAGE_URL ""
#define STDC_HEADERS 1
#define HAVE_SYS_TYPES_H 1
#define HAVE_SYS_STAT_H 1
#define HAVE_STDLIB_H 1
#define HAVE_STRING_H 1
#define HAVE_MEMORY_H 1
#define HAVE_STRINGS_H 1
#define HAVE_INTTYPES_H 1
#define HAVE_STDINT_H 1
#define HAVE_UNISTD_H 1
#define HAVE_FCNTL_H 1
#define HAVE_NET_IF_H 1

/* end confdefs.h.  */
/* Define $2 to an innocuous variant, in case <limits.h> declares $2.
   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
#define pcap_loop innocuous_pcap_loop

/* System header to define __stub macros and hopefully few prototypes,
    which can conflict with char $2 (); below.
    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
    <limits.h> exists even on freestanding compilers.  */

#ifdef __STDC__
# include <limits.h>
#else
# include <assert.h>
#endif

#undef pcap_loop

/* Override any GCC internal prototype to avoid an error.
   Use char because int might match the return type of a GCC
   builtin and then its argument prototype would still apply.  */
#ifdef __cplusplus
extern "C"
#endif
char pcap_loop();
/* The GNU C library defines this for functions which it implements
    to always fail with ENOSYS.  Some functions are actually named
    something starting with __ and the normal name is an alias.  */
#if defined __stub_pcap_loop || defined __stub___pcap_loop
choke me
#endif

int
main ()
{
return pcap_loop();
  ;
  return 0;
}

首先编译成 LLVM 的文本 IR 文件,然后用 opt 运行 pass 生成 DFSan 处理之后的 IR 文件,然后直接用编辑器打开这个 IR 文件,就可以看到 pcap_loop 引用被改写了:

clang -S -emit-llvm conftest.c -o conftest.ll
opt -load ../ipl-modeling/install/pass/libLoopHandlingPass.so -load ../ipl-modeling/install/pass/libDFSanPass.so -chunk-dfsan-abilist=../ipl-modeling/install/rules/dfsan_abilist.txt -o conftest_dfsan.ll conftest.ll -chunk-dfsan-abilist=../ipl-modeling/install/rules/angora_abilist.txt -S -dfsan_pass

image.png

解决方案#

下载 libpcap 的源码,在编译时使用 libDFSanPass 来对 pcap_loop 的函数定义进行改写,这里可以直接使用 modeling 模块的 test-clang,并且 libpcap 仓库中有 build.sh,不需要手动 configure make。

CC=/home/ubuntu/NestFuzz/ipl-modeling/install/test-clang CXX=/home/ubuntu/NestFuzz/ipl-modeling/install/test-clang++ ./build.sh

编译好 libpcap 之后,可以用 nm 确认一下 pcap_loop 符号的全名是否发生了改变。

image.png

因为这里编译 libpcap 的目录仅仅是为了后续编译 tcpdump,所以也可以不进行 make install。tcpdump 的 configure 脚本会在父目录里寻找 libpcap 目录。不过 tcpdump 目录下的 build.sh 脚本中的

--disable-local-libpcap

需要被删掉,这样才能让 configure 脚本从父目录寻找并采用 libpcap。用 test-clang 重新编译 tcpdump 就可以啦~

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.