起因#
事情是這樣的。
我想要復現 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 工具鏈,進行編譯。
- 構建 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 的可執行程序。
- 構建用於 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)
- 接著構建用於 modeling 的 target。和上面一步不同是,這一步所用的 CC 和 CXX 編譯器需要用到之前生成的 modeling 目錄下的 test-clang 和 test-clang++ 程序,這兩個程序對 clang 做了一層包裝,插入了額外的一些參數,通過傳入 DFSan 的 pass 來實現動態數據流分析。
問題出現#
從這裡開始,事情向著和 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 時,會報錯:
在 config.log 中的最後一個報錯如下:
在 ld 鏈接的過程中,發現被使用到的符號 dfs$pcap_loop
不存在。我一開始以為 dfs 這個單詞可能是 deep-first-search(深度優先)的意思,沒有什麼其他的特殊含義,即找不到的符號實際上就是 pcap_loop
變量或者函數。實際上 pcap_loop
是 the-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$
。
DFSan 原理#
DFSan 在 LLVM 的 pass 層級上實現了動態數據流分析,通過在函數調用、運算符等位置改寫代碼來實現污點追蹤。所謂的污點追蹤(Taint Tracking),就是給指定的變量打上污點標籤(taint label),當這個變量的值會隨著程序執行的過程,被其他指令使用到,例如作為參數傳入某個函數,被賦予某個變量,被加到某個數字上去時,函數的返回值或者收到影響的變量也會被打上標籤。
DFSan 實現污點追蹤的原理其實很簡單(LLVM 介紹 DFSan 的文檔),對於函數的傳參和返回值,DFSan 會改寫這個函數的簽名,在參數列表的末尾加上每個參數對應的 label 參數,並且額外 return 一個對應於返回值 label 變量。
DFSan 允許開發者提供一個 ABI List,來指定對涉及到的函數的重寫行為:
對於添加一個 dfs$
前綴的原因,DFSan 也解釋了:
注意:在 LLVM-10/11 中,採用了添加
dfs$
前綴的方式,在最新的 LLVM-19 中,已經變成添加後綴.dfsan
了。
鏈接器進行鏈接的時候只會匹配符號名,而不会像編譯器進行函數重載那樣 “認真” 的選擇被調用的函數定義(當然有的編譯器會將參數列表和返回值以縮寫的方式包含在符號名裡面),這樣對於相同的函數名而言,就無法區分哪些是被 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
解決方案#
下載 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 符號的全名是否發生了改變。
因為這裡編譯 libpcap 的目錄僅僅是為了後續編譯 tcpdump,所以也可以不進行 make install。tcpdump 的 configure 腳本會在父目錄裡尋找 libpcap 目錄。不過 tcpdump 目錄下的 build.sh 腳本中的
--disable-local-libpcap
需要被刪掉,這樣才能讓 configure 腳本從父目錄尋找並採用 libpcap。用 test-clang 重新編譯 tcpdump 就可以啦~