起因#
事情是这样的。
私はNestFuzzという論文の結果を再現したいと思い、著者が提供した Github リポジトリfdu-sec/NestFuzzを見つけました。この論文の実験部分で評価された可能性のある脆弱性のあるプログラムの中で、最も興味があるのは tcpdump です。しかし、リポジトリの README には tiff が記載されています。設定と実行プロセスは大差ないはずですが、実際にはいくつかの微妙な違いがある可能性があります。
README の指示に従って、まず NestFuzz の fuzzer をコンパイルします:
cd NestFuzz
make
この時点で、NestFuzz ディレクトリ内にコンパイルされた afl-fuzz プログラムが生成されました。次に、NestFuzz のモデリングコンポーネントを追加でコンパイルする必要があります。これは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
# rustをインストールする
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# その他の依存関係
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
次に、ターゲットをコンパイルして構築する必要があります。the-tcpdump-group/tcpdumpから master の最新バージョンをクローンし、fuzzing 用と modeling 用の実行可能プログラムをそれぞれ構築します。
- fuzzing 用のターゲットを構築する
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 用のターゲットを構築します。上記のステップとは異なり、このステップで使用する CC および CXX コンパイラは、以前に生成された modeling ディレクトリ内の test-clang および test-clang++ プログラムを使用する必要があります。これらのプログラムは clang に対してラッパーを追加し、追加のパラメータを挿入し、DFSan のパスを通じて動的データフロー分析を実現します。
問題が発生#
ここから、事態は 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 という単語が深さ優先探索(深度優先)の意味かもしれないと思い、特に他の特別な意味はないと考え、実際に見つからないシンボルはpcap_loop
変数または関数であると考えました。実際、pcap_loop
はthe-tcpdump-group/libpcapが提供する関数です。したがって、libpcap パッケージがリンク時にコンピュータで見つからなかった可能性があります。
libpcap-dev
を apt でインストールしようとしましたが、インストール後にpcap-config
を使用してコンパイルオプションを生成できることから、libpcap のインストールには問題がないことがわかりました。しかし、configure は依然として同じエラーを報告します。
私は一日中悩み、論文で使用されている ubuntu にシステム環境を変更することさえ試みましたが、うまくいきませんでした。dfs が DFSan の略か、少なくとも DFSan に関連していることに気づくまで、そうでした。
clang が読み込む 2 つのパス:libLoopHandlingPass と libDFSanPass は、llvm-10 のディレクトリ内でソースコードを見つけることができ、LoopHandlingPass.cpp と DataFlowSanitizer.cpp としてそれぞれ見つけることができます。検索すると、後者が pcap_loop 関数名の前にdfs$
を追加していることがわかります。
DFSan の原理#
DFSan は LLVM のパスレベルで動的データフロー分析を実現しており、関数呼び出しや演算子などの位置でコードを書き換えることで汚染追跡を実現しています。汚染追跡(Taint Tracking)とは、指定された変数に汚染ラベル(taint label)を付け、その変数の値がプログラムの実行過程で他の命令に使用されるとき、たとえば、ある関数に引数として渡されたり、ある変数に代入されたり、ある数値に加算されたりすると、関数の戻り値や影響を受ける変数にもラベルが付けられることを指します。
DFSan が汚染追跡を実現する原理は実際には非常にシンプルです(LLVM が DFSan を紹介する文書)。関数の引数と戻り値に対して、DFSan はこの関数のシグネチャを書き換え、引数リストの末尾に各引数に対応するラベル引数を追加し、さらに戻り値ラベル変数に対応するものを返します。
DFSan は開発者に ABI リストを提供することを許可し、関与する関数の書き換え動作を指定できます:
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 を実行してパスを実行して 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 を再コンパイルすれば完了です。