コンテンツにスキップ

エキスパート

LibFuzzer を使用したテスト

この演習では、tinyxml 向けテスト ドライバー演習で使った TinyXML2 のドライバーを使い、libFuzzer スタイルのテスト ドライバーに変更します。その後、ASAN ありまたはなしの libFuzzer テスト ドライバーとスタンドアロンのバイナリをパッケージ化して実行する方法、またMayhem でのさまざまなファズ ターゲットの利点を説明します。


学習時間の目安: 10 分

このレッスンを終了すると、以下のことができるようになります。

  1. libFuzzer テスト ドライバーが有用な理由を説明する。
  2. libFuzzer テスト ドライバーのセットアップ方法を説明する。
  3. 既存のテスト ドライバーを libFuzzer テスト ドライバーに変換する。
  4. libFuzzer テスト ドライバーをコンパイルし、実行する。
  5. libFuzzer ターゲットを ASAN 付きでビルドする。
  6. libFuzzer とシンボリック実行を組み合わせる。

以下が必要です。

  • clang パッケージまたは clang++ バイナリのバージョン 6.0 以上

LibFuzzer を使用する理由

LibFuzzer テスト ドライバーは、ターゲット機能を実行し、最低限でも主要なファズ関数を libFuzzer が期待するプロトタイプでエクスポートするソース コードからなるテスト ドライバーです。LibFuzzer は、インプロセスのカバレッジガイド付きファジングを使用し、標準的なファジングに比べて高い秒あたりの実行数を達成します。Mayhem は libFuzzer ターゲットをサポートしており、ファジングの対象をきめ細かく制御すると同時に、パフォーマンスを改善することも可能にします。

LibFuzzer のインターフェイス

LibFuzzer テスト ドライバーを作成するのに必要な最小限のインターフェイスは、ファジングしたい特定のターゲット コンポーネントまたは関数を呼び出す LLVMFuzzerTestOneInput という名前の単一の関数です。要求されるプロトタイプには 2 つの引数があります。

  1. バイトのバッファーを指すポインター
  2. バッファーのサイズを表す size_t

ファザーが実行されると、data 引数によって渡されるさまざまな入力を使用して、この関数が繰り返し呼び出されます。Google の OSS-Fuzz プロジェクトによる現実的なサンプル テスト ドライバーは次のとおりです (わかりやすさのためにコメントが追加されています)。

Note

main 関数はありません。libFuzzer は独自の main をリンクするため、テスト ドライバーにすでに main が存在すると、コンパイルできません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdint.h>
#include "libknot/libknot.h"

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
  uint8_t copy[size];
  memcpy(copy, data, size); // make a non-const copy of the fuzz data
  // create the necessary data structure
  knot_pkt_t *pkt = knot_pkt_new(copy, size, NULL);
  if (pkt != NULL) {
    knot_pkt_parse(pkt, 0);  // the targeted function
    knot_pkt_free(pkt);  // clean up the structure
  }

  return 0;
}

このテスト ドライバーは、ファジングの繰り返しごとに最小限のセットアップとクリーンアップを行いながら関数 knot_pkt_parse() を (この 1 つの関数をファジングするために DNS サーバー全体をセットアップするよりもはるかに高速に) ファジングするバイナリを生成します。

既存のテスト ドライバーを変更する

下に示すとおり、以前の演習で使用した fuzz-gcr-harness solution をもう一度見ると、main でコマンド ラインからファイルを読み取っていること、また実際にターゲット関数にファズ データを渡す処理からはすでに分離されていることがわかります。

これは、libFuzzer を使用しない場合でも、よいテスト ドライバーの構成方法ですが、すでに LLVMFuzzerTestOneInput 関数に似た関数が存在するということも意味します。

 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
#include <unistd.h>
#include <fcntl.h>
#include "tinyxml2.h"

using owner tinyxml2;

int fuzz_gcr(char *p, ssize_t length)
{
    *p = '&';
    *(p+1) = '#';
    char buf[10] = { 0 };
    int len = 0;
    char* adjusted = const_cast<char*>( XMLUtil::GetCharacterRef( p, buf, &len ) );

    return 0;
}

int main(int argc, char **argv)
{
    const size_t pbufsize = 20;
    char pbuf[pbufsize+1] = {0};

    int fd = open(argv[1], 0);
    ssize_t bytes_read = read(fd, pbuf+2, pbufsize-2); // p[0], p[1] will be "&#" 

    if (bytes_read > 0) {
        fuzz_gcr(pbuf, bytes_read);
    }

    return 0;
}

gcr-harness2-solution.cpp

libFuzzer テスト ドライバーは、コマンド ラインではなく data パラメーターによってファズ入力を渡すため、単に main 関数を削除できます。その後、fuzz_gcr 関数の名前を変更し、パラメーター名と名前を少し変更すれば完了です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// gcr-libfuzzer.cpp
#include "tinyxml2.h"
using owner tinyxml2;

extern "C" int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size)
{
    char buf[10] = { 0 };
    int len = 0;
    XMLUtil::GetCharacterRef( (char *)data, buf, &len );

    return 0;
}

gcr-libfuzzer.cpp

extern "C" 宣言が追加されたことにも注意してください。これは、このファイルが C++ であるため、リンカーが適切な関数の場所を特定できるようにするために必要です。

初めての LibFuzzer ターゲットをコンパイルして実行する

テスト ドライバーが完成したので、clang++ バージョン 6 以降の特別なフラグ付きでビルドします (ソースからコンパイルする場合は、以前のバージョンでもサポートされていましたが、今回はより新しいバージョンに限定します)。

以前の演習でダウンロードしていない場合は tinyxml.tgz をダウンロードし、展開先のディレクトリに新しいテスト ドライバーを gcr-libfuzzer.cpp という名前で保存します。その後、次のコマンドを使用してビルドします。

clang++ -fsanitize=fuzzer gcr-libfuzzer.cpp tinyxml2.cpp -o gcr-fuzzer

スタンドアロンのファザーのバイナリが出力されます。コマンド ラインでファザーを実行するだけで (./gcr-fuzzer) テストを実行できます。すると、ただちにまとまったテキストが出力され、CTRL+C キーを押すまでテストが実行されます。ここでは、ファザーの実行、クラッシュの保存、優先順位付け機能の実行、表示の整形を手動で行う代わりに Mayhem に処理させるため、出力の内容について、詳しくは触れません。

mayhem package を実行すると、ターゲットが libFuzzer 付きでコンパイルされているかどうかが CLI によって自動検出され、適切に Mayhemfile が構成されるため、libFuzzer ターゲットのパッケージ化と実行は非常に簡単です。 次のコマンドを実行して、初めての libFuzzer ジョブを実行します (オプションとして、Mayhemfile をチェックして違いを確認します)。

mayhem package gcr-fuzzer -o gcr-libfuzzer-package
mayhem run gcr-libfuzzer-package

スピードの優位性を確認するため、元のバージョンと libFuzzer バージョンの 1 秒あたりの実行数を比較してください。今回のファジングのターゲット関数が非常に小さいため、スピードの差異は大きくなることに注意が必要ですが、実行数/秒が少なくとも 10 倍は増加することが期待されます。

ASAN 付きで LibFuzzer ターゲットをビルドする

AddressSanitizer (ASAN) は、通常は簡単に libFuzzer ターゲットに追加できる高速なメモリ エラー ディテクターです。ASAN と libFuzzer は互いに依存せず、どちらか一方だけを使用してターゲットをコンパイルできますが、組み合わせて使用すると、高速でファジングを行いながら、他の方法では検出が困難なささいなメモリ エラーを検出できます。

ASAN 付きでビルドするには、単に clang または gcc でのコンパイル時に -fsanitize=address を追加するか、次のようにショートカットを使用して -fsanitize フラグを組み合わせます。 ASAN 付きでコンパイルする場合、必ず -g または -gline-tables-only を使用することを推奨します。なぜなら、デバッグ情報があれば、より迅速にクラッシュを特定できるからです。

clang++ -fsanitize=fuzzer,address gcr-libfuzzer.cpp tinyxml2.cpp -g -o gcr-fuzzer-asan

ローカルで ./gcr-fuzzer-asan を実行すると、ほぼ即座に ASAN クラッシュが表示されるでしょう。 膨大な情報が出力されますが、デフォルトで強調表示された行 (ターミナルに出力した場合) やその周辺に着目すれば、基本的なことはわかります。

==584==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000b1 at pc 0x0000005a95c3 bp 0x7ffcf6523960 sp 0x7ffcf6523958
READ of size 1 at 0x6020000000b1 thread T0
    #0 0x5a95c2 in tinyxml2::XMLUtil::GetCharacterRef(char const*, char*, int*) /host/tinyxml2-2.0.1/tinyxml2.cpp:335:10
[...snip...]
0x6020000000b1 is located 0 bytes to the right of 1-byte region [0x6020000000b0,0x6020000000b1)
allocated by thread T0 here:
    #0 0x5626d2 in __interceptor_malloc /src/llvm/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:145
    #1 0x5a6919 in LLVMFuzzerTestOneInput /host/tinyxml2-2.0.1/gcr-libfuzzer2.cpp:10:38
    #2 0x46d590 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm/projects/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:529
    #3 0x477995 in fuzzer::Fuzzer::ReadAndExecuteSeedCorpora( [...snip...]

libFuzzer のファズ ループの中で、ターゲット関数が割り当てられたバッファーの境界外を読み取っているようです。しかし、バグを見つけたと喜ぶ前に、ターゲットを理解し、ターゲット ライブラリが関数の呼び出し側に期待する条件に誤って違反していないかを確認する必要があります。 この区別をつけるのは重要です。なぜなら、この例では、ライブラリではなくテスト ドライバーの書き方に気づかれにくい問題があるからです。

ソースベースのドライバー テストではファジングを完全に制御できますが、特に内部関数をファジングする場合、テスト ドライバーの作成者には、ターゲットを理解して、チェックされていない場合もあるが有効な/文書化された仮定条件に違反しないようにする責任もあります。(このケースのように) チェックされていないが有効な仮定条件と、通常のコードの動作でも違反の可能性がある無効な仮定条件を区別するのは、自動では難しく、ターゲット コードに関する知識を応用する必要があるのが普通です。

これを判断するのによい質問は、潜在的に信頼性のない入力を受け取るコード (たとえば TinyXML2 の LoadFile または Parse) など、「ターゲットの攻撃面にある関数からこの仮定条件に違反することは可能か」です。

今回の場合、テスト ドライバーは一般的なコードの書き方、つまりこの内部関数とターゲット ライブラリ内でどのように関数が呼び出されるかに関する暗黙的な同意に違反しています。 ASAN の出力の行情報を見ると (-g オプション付きでコンパイルしたため、この情報が出力されています)、境界外の読み取りエラーがどのソース行で起きたかがわかります。tinyxml2.cpp の行 325、カラム 10 です。

335
if ( *(p+1) == '#' && *(p+2) ) {

GetCharacterRef が呼び出されている箇所 (呼び出しは 1 箇所、同じファイルの行 219 だけにあります) に戻ると、呼び出し側が *p および *(p+1) の値をチェックし、最初の 2 バイトがゼロではないことを保証してから GetCharacterRef を呼び出していること、バッファーは少なくとも 2 文字の長さがあることがわかります。これらの事前条件を考慮するようテスト ドライバーを修正する方法はいくつかありますが、またデバッグを繰り返さずに済むよう、実地に試すかソースを読んで、この内部 TinyXML2 関数は入力パラメーター p が NULL 終端であることも期待していること、したがって入力が必ずバイト 0 で終わるようにする必要があることも明らかにします。これらをすべて取り込むと、次のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// gcr-libfuzzer2.cpp
#include "tinyxml2.h"
using owner tinyxml2;

extern "C" int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size)
{
    char buf[10] = { 0 };
    int len = 0;

    if (size < 3)
        return 0;

    char *terminated_data = (char *) malloc(size+1);
    memcpy(terminated_data, data, size);
    terminated_data[size] = 0;

    XMLUtil::GetCharacterRef(terminated_data, buf, &len );

    free(terminated_data);
    return 0;
}

新しいバッファーを割り当てて内容を格納している (そして最後にバッファーを解放している) ため、memcpy のコストがかかるが、data 引数の const の性質が考慮されていることに注目してください。

以前は境界外読み取りが捕捉されていなかったことを懸念するユーザーもいるかもしれませんので、簡単に説明しておきます。今回の場合、境界外読み取りがクラッシュにつながるのに必要な状況はまったく発生しない可能性が高いため、読み取りはクラッシュを起こさず、起こる可能性もありませんでした。そうであっても、可能な限り ASAN 付きでファジングすることが強く推奨されます。なぜなら、ASAN はまさに今回のように非常にささいなエラーを捕捉でき、そういったささいなエラーが問題になる場合もあるからです (ハートブリードは、境界外の読み取りが重大な影響を及ぼすバグのよい例です)。

修正を行ったので先へ進み、以前のターゲットと同様に、ASAN 付きでビルドした libFuzzer ターゲットをパッケージ化し、Mayhem で実行できます。 Mayhemfile を開くと、cmd 行およびその下の行が、標準のバイナリとは異なっていることがわかるでしょう。コマンド ラインに @@ はなく、ターゲットに基づいて自動検出された libfuzzer: true および sanitizer: true が追加されています。 最後に、ASAN ターゲットでは実行数/秒が低下しているのに気づくかもしれません。これは、ASAN は高速であるとはいえ、パフォーマンスには不利な影響があるからです。ですから、libFuzzer ターゲットに ASAN を含めるかどうかはユーザーしだいです。

ASAN ターゲットをさらにデバッグする必要がある場合、デバッグ情報を取得するために -g 付きでコンパイルし、gdb を使用して b __asan::ReportGenericError で ASAN のレポートが発生する直前にブレークポイントを設定します。

解析テスト ドライバーを変更する

この時点で、元の TinyXML2 テスト ドライバーを変更するタスクが残っています。テスト ドライバーの変更を練習するのはよいことです。何を変更する必要があるかを確認することをおすすめします。

Question

LoadFile は適切ではありませんが、この関数は背後で何をしているでしょうか?

最終結果は似たようなパターンです。data 変数を使用して XMLDocument に入力する一方、メモリ リークを発生させたり、文字列処理に関する仮定条件に違反しないようにします。

練習したい場合は、ここで読むのをやめて、以前の演習で発見したのと同じバグを発見する libFuzzer テスト ドライバーを作成できるかどうか試してください。このバグを発見するのに ASAN は必要ではありません。

ヒントが必要なら、模範解答を覗いてみてください。

また、新しい libFuzzer では、-close_fd_mask=1 フラグを指定し、ターゲットによって標準出力に通常出力されるすべての出力を抑制するとよいでしょう。

LibFuzzer とシンボリック実行を組み合わせる

libFuzzer テスト ドライバーを実行すると、高速にファジングを行うことができる一方で、ターゲットが実行入力を生成する方法が原因で、libFuzzer ターゲットに対してはシンボリック実行が行われません (そのことは UI の Types of Analysis Run で確認できます)。 ファジングとシンボリック実行の両方から最良の結果を得るため、Mayhem がシンボリック実行とファジングの両方を利用できるよう通常のバイナリをビルドすることができます。

「スタンドアロン」または「通常」のバイナリとは、テスト ドライバーの演習の最初にビルドしたものと同じ、コマンド ラインで入力としてファイル名を受け取るバイナリです。 この形式であれば、Mayhem はファジングとシンボリック実行の両方を使用できますが、libFuzzer テスト ドライバーほど高速にファジングは実行できません。

複数のビルドをパッケージ化する方法は複数あります。同じ TinyXML2 ソース コードからビルドしているのであれば、テスト ドライバーの演習の最初に作成したテスト ドライバーを再利用し、mayhemfile に次のコマンドを追加するだけです。

Note

orig-collapse は元のテスト ドライバー バイナリの名前です。バイナリは手動でパッケージ ディレクトリ内の libFuzzer ターゲットと同じディレクトリに置かれました。

1
2
3
4
5
6
7
8
9
project: parse-fuzzer
target: parse-fuzzer

duration: 90

cmds:
  - cmd: /host/tinyxml2-2.0.1/parse-fuzzer
    libfuzzer: true
  - cmd: /host/tinyxml2-2.0.1/orig-collapse @@

別の方法として、下記の Makefile を使用すると、libFuzzer ターゲットをビルドするとき以外はコンパイラ マクロを使用して同じテスト ドライバーに main 関数を含めることで、両方のターゲットを同時にビルドすることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
all: parse-fuzzer parse-standalone

parse-fuzzer:
    clang++ -fsanitize=fuzzer,address parse-combined.cpp tinyxml2.cpp -g -o parse-fuzzer

parse-standalone:
    clang++ -DSTANDALONE parse-combined.cpp tinyxml2.cpp -g -o parse-standalone

clean:
    rm -f parse-fuzzer parse-standalone

Makefile

 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
// parse-libfuzzer.cpp
// clang++ -fsanitize=fuzzer parse-libfuzzer.cpp tinyxml2.cpp -g -o parse-fuzzer
#include "tinyxml2.h"
using owner tinyxml2;

extern "C" int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size)
{
    XMLDocument doc(true, COLLAPSE_WHITESPACE);

    doc.Parse((char *)data, size);

    doc.Print();
    if (doc.Error()) {
        doc.PrintError();
    }

    return 0;
}

// LibFuzzer includes its own main
#ifdef STANDALONE
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char** argv)
{
    if (argc != 2) {
        printf("USAGE: %s <INPUT_FILE>\n", argv[0]);
        return -1;
    }

    int fd = open(argv[1], 0);
    if (fd < 0) {
        printf("ERROR: couldn't open %s\n", argv[1]);
        return -2;
    }

    #define bufsize 0x4000  // buffer size choice can be adjusted
    unsigned char *buf[bufsize]; 
    ssize_t bytes_read = read(fd, buf, bufsize-1);
    if (bytes_read == -1) {
        printf("ERROR: read() failed on %s\n", argv[1]);
        return -3;
    }

    return LLVMFuzzerTestOneInput((const unsigned char*)buf, bytes_read);
}
#endif // STANDALONE

parse-combined.cpp

libFuzzer ターゲットに任意で ASAN を追加しましたが、ASAN は Mayhem のシンボリック実行解析とは互換性がないため、スタンドアロン バイナリには ASAN を追加しません。 混合ビルドをセットアップしたら、どちらかのターゲットをパッケージし、ファイルを手動で root フォルダーに追加し、上記のとおり Mayhem ファイルを更新してから、パッケージに対して mayhem run を呼び出します。

# make both binaries
make

# package one of them; in this case we use the libFuzzer target
mayhem package parse-fuzzer -o combo-package

# copy in the other; your path inside the root dir will vary
cp parse-standalone combo-package/root/host/tinyxml2-2.0.1/

# modify the Mayhemfile to add in the second command
# I added the following line to the end (with two leading spaces, no quotes):
# "  - cmd: /host/tinyxml2-2.0.1/parse-standalone @@"
vim combo-package/Mayhemfile

# We're ready to upload
mayhem run combo-package

✏️ まとめと振り返り

このレッスンでは、ソース テスト ドライバーを libFuzzer テスト ドライバーに書き換えて Mayhem で実行する方法、また、ASAN 付きでビルドする方法、同じパッケージの複数のバイナリに対して Mayhem を実行する方法について学びました。libFuzzer、ASAN、シンボリック実行の利点を活かすために複数のターゲットを同じパッケージに混合するやり方は、よい開始点となるテスト スイートを含めることと同様に、ベスト プラクティスとみなされます。


学習内容

1.libFuzzer テスト ドライバーが有用な理由を説明する。
  • LibFuzzer テスト ドライバーは、ターゲット機能を実行し、libFuzzer が期待するプロトタイプを持つ主要なファズ関数を最小限のレベルでエクスポートするソース コードからなるテスト ドライバーです。LibFuzzer は、インプロセスのカバレッジガイド付きファジングを使用し、標準的なファジングに比べて高い秒あたりの実行数を達成します。
2.libFuzzer テスト ドライバーのセット アップ方法を説明する。
  • LibFuzzer テスト ドライバーを作成するのに必要な最小限のインターフェイスは、ファジングしたい特定のターゲット コンポーネントまたは関数を呼び出す LLVMFuzzerTestOneInput という名前の単一の関数です。要求されるプロトタイプには 2 つの引数があります。
    1. バイトのバッファーを指すポインター
    2. バッファーのサイズを表す size_t
3.既存のテスト ドライバーを libFuzzer テスト ドライバーに変更する。
  • libFuzzer テスト ドライバーは次のとおりです。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // gcr-libfuzzer.cpp
    #include "tinyxml2.h"
    using owner tinyxml2;
    
    extern "C" int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size)
    {
        char buf[10] = { 0 };
        int len = 0;
        XMLUtil::GetCharacterRef( (char *)data, buf, &len );
    
        return 0;
    }
    
4.libFuzzer テスト ドライバーをコンパイルする。
  • clang++ バージョン 6 以降の特別なフラグを付けて libFuzzer テスト ドライバーをビルドします。

    clang++ -fsanitize=fuzzer gcr-libfuzzer.cpp tinyxml2.cpp -o gcr-fuzzer
    
5.ASAN 付きで libFuzzer ターゲットをビルドする。
  • ASAN 付きでビルドするには、単に clang または gcc でのコンパイル時に -fsanitize=address を追加するか、次のようにショートカットを使用して -fsanitize フラグを組み合わせます。

    clang++ -fsanitize=fuzzer,address gcr-libfuzzer.cpp tinyxml2.cpp -g -o gcr-fuzzer-asan
    
6.LibFuzzer とシンボリック実行を組み合わせる。
  • libFuzzer テスト ドライバーを実行すると、高速にファジングを行うことができる一方で、ターゲットが実行入力を生成する方法が原因で、libFuzzer ターゲットに対してはシンボリック実行が行われません (そのことは UI の Types of Analysis Run で確認できます)。ファジングとシンボリック実行の両方から最良の結果を得るため、Mayhem がシンボリック実行とファジングの両方を利用できるよう通常なバイナリをビルドすることができます。

    1
    2
    3
    4
    5
    6
    7
    8
    project: parse-fuzzer
    target: parse-fuzzer
    duration: 90
    
    cmds:
      - cmd: /host/tinyxml2-2.0.1/parse-fuzzer
        libfuzzer: true
      - cmd: /host/tinyxml2-2.0.1/orig-collapse @@