libFuzzer – カバレッジガイド付きファジングテスト用ライブラリ。

はじめに

LibFuzzerは、インプロセス型のカバレッジガイド付き進化型ファジングエンジンです。

LibFuzzerはテスト対象のライブラリとリンクされ、特定のファジングエントリポイント(別名「ターゲット関数」)を介してファジングされた入力をライブラリに供給します。その後、ファザーはコードのどの領域に到達したかを追跡し、コードカバレッジを最大化するために入力データのコーパスに対して変更を生成します。LibFuzzerのコードカバレッジ情報は、LLVMのSanitizerCoverage計測によって提供されます。

連絡先: libfuzzer(#)googlegroups.com

ステータス

LibFuzzerの元の作成者は、アクティブな作業を停止し、別のファジングエンジンであるCentipedeの開発に切り替えました。LibFuzzerは重要なバグは修正されるため、依然として完全にサポートされています。ただし、バグ修正以外の主要な新機能やコードレビューは期待しないでください。

バージョン

LibFuzzerには、対応するバージョンのClangが必要です。

はじめに

ファズターゲット

ライブラリでlibFuzzerを使用する最初のステップは、 *ファズターゲット* を実装することです。ファズターゲットとは、バイトの配列を受け取り、テスト対象のAPIを使用してこれらのバイトで何か興味深い処理を行う関数です。例:

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Values other than 0 and -1 are reserved for future use.
}

このファズターゲットは、libFuzzerに依存していないため、AFLRadamsaなどの他のファジングエンジンで使用することも可能です。推奨されます。

ファズターゲットについて覚えておくべき重要な点

  • ファジングエンジンは、同じプロセス内でさまざまな入力を使用してファズターゲットを何度も実行します。

  • あらゆる種類の入力(空、巨大、不正など)に耐える必要があります。

  • いかなる入力に対してもexit()してはなりません。

  • スレッドを使用できますが、理想的には、関数の最後にすべてのスレッドを結合する必要があります。

  • 可能な限り決定論的である必要があります。入力バイトに基づかない非決定論(例:ランダムな決定)は、ファジングを非効率にします。

  • 高速である必要があります。3次以上の複雑さ、ログ、過剰なメモリ消費は避けてください。

  • 理想的には、グローバル状態を変更すべきではありません(ただし、厳密なものではありません)。

  • 通常、ターゲットが狭いほど優れています。たとえば、ターゲットが複数のデータ形式を解析できる場合、形式ごとに複数のターゲットに分割します。

ファザーの使用

最近のClangバージョン(6.0以降)にはlibFuzzerが含まれており、追加のインストールは必要ありません。

ファザーバイナリをビルドするには、コンパイルとリンク時に-fsanitize=fuzzerフラグを使用します。ほとんどの場合、libFuzzerをAddressSanitizer(ASAN)、UndefinedBehaviorSanitizer(UBSAN)、またはその両方と組み合わせることをお勧めします。 MemorySanitizer(MSAN)を使用してビルドすることもできますが、サポートは実験段階です。

clang -g -O1 -fsanitize=fuzzer                         mytarget.c # Builds the fuzz target w/o sanitizers
clang -g -O1 -fsanitize=fuzzer,address                 mytarget.c # Builds the fuzz target with ASAN
clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c # Builds the fuzz target with a part of UBSAN
clang -g -O1 -fsanitize=fuzzer,memory                  mytarget.c # Builds the fuzz target with MSAN

これにより、必要な計測が行われ、libFuzzerライブラリとのリンクも実行されます。-fsanitize=fuzzerは、libFuzzerのmain()シンボルをリンクすることに注意してください。

独自のmainシンボルを必要とする実行ファイルもコンパイルする大規模なプロジェクトのCFLAGSを変更する場合は、リンクせずに計測のみを要求することをお勧めします。

clang -fsanitize=fuzzer-no-link mytarget.c

その後、リンク段階で-fsanitize=fuzzerを渡すことで、libFuzzerを目的のドライバにリンクできます。

コーパス

libFuzzerのようなカバレッジガイド付きファザーは、テスト対象のコードのサンプル入力のコーパスに依存します。このコーパスには、理想的には、テスト対象のコードに対して多様な有効な入力と無効な入力が含まれている必要があります。たとえば、グラフィックライブラリの場合、最初のコーパスにはさまざまな小さなPNG/JPG/GIFファイルが含まれている可能性があります。ファザーは、現在のコーパス内のサンプル入力を基にしてランダムな変更を生成します。変更によってテスト対象のコードで以前にカバーされていなかったパスが実行されると、その変更は将来のバリエーションのためにコーパスに保存されます。

LibFuzzerは初期シードなしでも動作しますが、テスト対象のライブラリが複雑な構造化された入力を受け入れる場合、効率が低下します。

コーパスは、ファジングエントリポイントがまだ機能しており、テスト対象のコードですべてのサンプル入力が問題なく実行されることを確認するためのサニティ/回帰テストとしても機能します。

大規模なコーパス(ファジングによって生成されたもの、または他の手段で取得されたもの)がある場合、完全なカバレッジを維持しながら最小化したい場合があります。そのための一つの方法は、-merge=1フラグを使用することです。

mkdir NEW_CORPUS_DIR  # Store minimized corpus here.
./my_fuzzer -merge=1 NEW_CORPUS_DIR FULL_CORPUS_DIR

既存のコーパスにさらに興味深いアイテムを追加するためにも、同じフラグを使用できます。新しいカバレッジをトリガーする入力のみが最初のコーパスに追加されます。

./my_fuzzer -merge=1 CURRENT_CORPUS_DIR NEW_POTENTIALLY_INTERESTING_INPUTS_DIR

実行

ファザーを実行するには、最初に初期「シード」サンプル入力を保持するコーパスディレクトリを作成します。

mkdir CORPUS_DIR
cp /some/input/samples/* CORPUS_DIR

次に、コーパスディレクトリでファザーを実行します。

./my_fuzzer CORPUS_DIR  # -max_len=1000 -jobs=20 ...

ファザーは、新しい興味深いテストケース(つまり、テスト対象のコードで新しいパスのカバレッジをトリガーするテストケース)を発見すると、それらのテストケースをコーパスディレクトリに追加します。

デフォルトでは、ファジングプロセスは無期限に継続します(少なくともバグが見つかるまで)。クラッシュやサニタイザーのエラーは通常どおり報告され、ファジングプロセスが停止し、バグをトリガーした特定の入力がディスクに書き込まれます(通常はcrash-<sha1>leak-<sha1>、またはtimeout-<sha1>)。

並列ファジング

テスト対象のライブラリが独自のThreadを開始しない限り、各libFuzzerプロセスはシングルスレッドです。ただし、共有コーパスディレクトリで複数のlibFuzzerプロセスを並列に実行できます。これには、1つのファザープロセスによって見つかった新しい入力が他のファザープロセスで使用できるという利点があります(-reload=0オプションで無効にしない限り)。

これは主に-jobs=Nオプションによって制御されます。このオプションは、N個のファジングジョブを完了するまで実行する必要があることを示します(つまり、バグが見つかるか、時間/反復の制限に達するまで)。これらのジョブは、デフォルトでは使用可能なCPUコアの半分を使用する一連のワーカープロセスで実行されます。ワーカープロセスの数は-workers=Nオプションで上書きできます。たとえば、12コアのマシンで-jobs=30を実行すると、デフォルトで6つのワーカーが実行され、各ワーカーはプロセスの完了までに平均5個のバグを処理します。

フォークモード

実験的モード-fork=N(ここでNは並列ジョブの数です)は、個別のプロセス(forkだけでなくfork-execを使用)によるoom耐性、タイムアウト耐性、クラッシュ耐性ファジングを有効にします。

トップレベルのlibFuzzerプロセスはファジング自体を実行しませんが、最大N個の同時子プロセスを生成し、コーパスの小さなランダムなサブセットを提供します。子が終了すると、トップレベルのプロセスは、子によって生成されたコーパスをメインコーパスにマージします。

関連するフラグ

-ignore_ooms

デフォルトでTrue。子プロセスのファジング中にOOMが発生した場合、再現子がディスクに保存され、ファジングが継続されます。

-ignore_timeouts

デフォルトでTrue。-ignore_oomsと同じですが、タイムアウトの場合です。

-ignore_crashes

デフォルトでFalse。-ignore_oomsと同じですが、その他のすべてのクラッシュの場合です。

-jobs=N-workers=N-fork=Nで置き換える予定です。

再開とマージ

大規模なコーパスのマージには時間がかかる場合があり、プロセスがいつでもキルされる可能性のあるプリエンプティブなVMで実行することをお勧めします。マージをシームレスに再開するには、-merge_control_fileフラグを使用し、killall -SIGUSR1 /path/to/fuzzer/binaryを使用してマージを正常に停止します。例:

% rm -f SomeLocalPath
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-INNER: using the control file 'SomeLocalPath'
...
# While this is running, do `killall -SIGUSR1 my_fuzzer` in another console
==9015== INFO: libFuzzer: exiting as requested

# This will leave the file SomeLocalPath with the partial state of the merge.
# Now, you can continue the merge by executing the same command. The merge
# will continue from where it has been interrupted.
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-OUTER: non-empty control file provided: 'SomeLocalPath'
MERGE-OUTER: control file ok, 32 files total, first not processed file 20
...

オプション

ファザーを実行するには、コマンドライン引数として0個以上のコーパスディレクトリを渡します。ファザーはこれらの各コーパスディレクトリからテスト入力を読み込み、生成された新しいテスト入力は最初のコーパスディレクトリに書き戻されます。

./fuzzer [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]

ファザープログラムにファイルのリスト(ディレクトリではなく)が渡された場合、それらのファイルをテスト入力として再実行しますが、ファジングは実行しません。このモードでは、ファザーバイナリを回帰テスト(例:継続的インテグレーションシステム)として使用して、対象関数と保存された入力がまだ機能しているかどうかを確認できます。

最も重要なコマンドラインオプションは以下のとおりです。

-help

ヘルプメッセージを表示します(-help=1)。

-seed

乱数シード。0(デフォルト)の場合、シードが生成されます。

-runs

個々のテスト実行の数。-1(デフォルト)は無限に実行します。

-max_len

テスト入力の最大長。0(デフォルト)の場合、libFuzzerはコーパスに基づいて適切な値を推測しようとします(そしてそれを報告します)。

-len_control

最初に小さな入力を生成してから、時間とともに大きな入力を試行します。長さ制限の増加率を指定します(小さいほど速い)。デフォルトは100です。0の場合、すぐにmax_lenまでのサイズの入力を試行します。

-timeout

タイムアウト(秒単位)、デフォルトは1200。入力がこのタイムアウトよりも長くかかった場合、プロセスは失敗ケースとして扱われます。

-rss_limit_mb

メモリ使用量制限(MB単位)、デフォルトは2048。0を使用すると制限が無効になります。入力が実行するためにこの量のRSSメモリよりも多くを必要とする場合、プロセスは失敗ケースとして扱われます。この制限は、別のスレッドで毎秒チェックされます。ASAN/MSANなしで実行する場合は、「ulimit -v」を使用できます。

-malloc_limit_mb

0以外の場合、ターゲットが1回のmalloc呼び出しでこの数のMBを割り当てようとすると、ファザーは終了します。0(デフォルト)の場合、rss_limit_mbと同じ制限が適用されます。

-timeout_exitcode

libFuzzerがタイムアウトを報告した場合に使用される終了コード(デフォルトは77)。

-error_exitcode

libFuzzer自体(サニタイザーではなく)がバグ(リーク、OOMなど)を報告した場合に使用される終了コード(デフォルトは77)。

-max_total_time

正の場合、ファザーを実行する最大合計時間(秒単位)を示します。0(デフォルト)の場合、無期限に実行します。

-merge

1に設定されている場合、2番目、3番目などのコーパスディレクトリからの新しいコードカバレッジをトリガーするコーパス入力は、最初のコーパスディレクトリにマージされます。デフォルトは0です。このフラグは、コーパスを最小限にするために使用できます。

-merge_control_file

マージプロセスで使用されるコントロールファイルを指定します。マージプロセスが強制終了された場合、このファイルをマージを再開するのに適した状態で残そうとします。デフォルトでは、一時ファイルが使用されます。

-minimize_crash

1の場合、提供されたクラッシュ入力を最小化します。-runs=Nまたは-max_total_time=Nと共に使用して、試行回数を制限します。

-reload

1(デフォルト)に設定されている場合、コーパスディレクトリは定期的に再読み込みされ、新しい入力がないかチェックされます。これにより、他のファジングプロセスによって検出された新しい入力を検出できます。

-jobs

完了まで実行するファジングジョブの数。デフォルト値は0で、単一のファジングプロセスが完了するまで実行します。値が>=1の場合、ファジングを実行するこの数のジョブが、一連の並列個別のワーカープロセスで実行されます。そのような各ワーカープロセスは、stdout/stderrfuzz-<JOB>.logにリダイレクトされます。

-workers

ファジングジョブを完了するまで実行する同時ワーカープロセスの数。0(デフォルト)の場合、min(jobs, NumberOfCpuCores()/2)が使用されます。

-dict

入力キーワードの辞書を提供します。「辞書」を参照してください。

-use_counters

コードブロックがヒットする頻度の概算数を生成するためにカバレッジカウンターを使用します。デフォルトは1です。

-reduce_inputs

完全な機能セットを保持しながら、入力のサイズを削減しようとします。デフォルトは1です。

-use_value_profile

値プロファイルを使用してコーパスの拡張をガイドします。デフォルトは0です。

-only_ascii

1の場合、ASCII(isprint``+``isspace)入力のみを生成します。デフォルトは0です。

-artifact_prefix

ファジングアーティファクト(クラッシュ、タイムアウト、または低速入力)を$(artifact_prefix)fileとして保存するときに使用するプレフィックスを提供します。デフォルトは空です。

-exact_artifact_path

空の場合(デフォルト)は無視されます。空でない場合、失敗時(クラッシュ、タイムアウト)の単一のアーティファクトを$(exact_artifact_path)として書き込みます。これは-artifact_prefixをオーバーライドし、ファイル名にチェックサムを使用しません。複数の並列プロセスに対して同じパスを使用しないでください。

-print_pcs

1の場合、新しくカバーされたPCを出力します。デフォルトは0です。

-print_final_stats

1の場合、終了時に統計情報を表示します。デフォルトは0です。

-detect_leaks

1(デフォルト)の場合、LeakSanitizerが有効になっている場合、ファジング中にメモリリークを検出しようとします(つまり、シャットダウン時だけではありません)。

-close_fd_mask

起動時に閉じる出力ストリームを示します。注意してください。これにより、ターゲットコードからの診断出力(アサーション失敗時のメッセージなど)が削除されます。

  • 0(デフォルト):stdoutstderrも閉じません。

  • 1:stdoutを閉じます。

  • 2:stderrを閉じます。

  • 3:stdoutstderrの両方を閉じます。

フラグの完全なリストについては、-help=1を使用してファザーバイナリを実行してください。

出力

動作中は、ファザーはstderrに情報を表示します。たとえば、

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
...

出力の最初の部分には、現在の乱数シード(Seed:行に含まれます。これは-seed=Nフラグでオーバーライドできます)を含むファザーのオプションと構成に関する情報が含まれています。

さらに出力行は、イベントコードと統計情報の形式になっています。可能なイベントコードは次のとおりです。

READ

ファザーは、コーパスディレクトリから提供されたすべての入力サンプルを読み込みました。

INITED

ファザーは初期化を完了しました。これには、テスト対象のコードを介して初期入力サンプルをそれぞれ実行することが含まれます。

NEW

ファザーは、テスト対象のコードの新しい領域をカバーするテスト入力を作成しました。この入力は、プライマリコーパスディレクトリに保存されます。

REDUCE

ファザーは、以前に検出された機能をトリガーする、より良い(小さい)入力を見つけました(無効にするには-reduce_inputs=0を設定します)。

pulse

ファザーは2n個の入力を生成しました(ファザーがまだ機能していることをユーザーに安心させるために定期的に生成されます)。

DONE

ファザーは、指定された繰り返し回数制限(-runs)または時間制限(-max_total_time)に達したため、操作を完了しました。

RELOAD

ファザーは、コーパスディレクトリからの入力の定期的なリロードを実行しています。これにより、他のファザープロセスによって検出された入力を見つけることができます(「並列ファジング」を参照)。

各出力行には、次の統計情報も報告されます(0以外の値の場合)。

cov

現在のコーパスを実行することによってカバーされたコードブロックまたはエッジの総数。

ft

libFuzzerは、コードカバレッジを評価するために異なるシグナルを使用します。エッジカバレッジ、エッジカウンター、値プロファイル、間接的な呼び出し元/呼び出し先ペアなど。これらのシグナルを組み合わせたものを機能ft:)と呼びます。

corp

現在のインメモリテストコーパスとそのサイズ(バイト単位)のエントリ数。

lim

コーパス内の新しいエントリの現在の長さ制限。時間とともに増加し、最大長(-max_len)に達します。

exec/s

1秒あたりのファザー反復回数。

rss

現在のメモリ消費量。

NEWおよびREDUCEイベントの場合、出力行には、新しい入力を生成した変異操作に関する情報も含まれています。

L

新しい入力のサイズ(バイト単位)。

MS: <n> <operations>

入力を生成するために使用された変異操作の数とリスト。

簡単な例

「HI!」という入力を受信した場合に何か興味深いことをする単純な関数。

cat << EOF > test_fuzzer.cc
#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;
}
EOF
# Build test_fuzzer.cc with asan and link against libFuzzer.
clang++ -fsanitize=address,fuzzer test_fuzzer.cc
# Run the fuzzer with no corpus.
./a.out

すぐにエラーが発生するはずです。

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
==31511== ERROR: libFuzzer: deadly signal
...
artifact_prefix='./'; Test unit written to ./crash-b13e8756b13a00cf168300179061fb4b91fefbed

詳細な例

現実世界のファズターゲットとそれらが検出するバグの例は、http://tutorial.libfuzzer.infoにあります。その他にも、Heartbleedを1秒で検出する方法を学ぶことができます。

高度な機能

辞書

libFuzzerは、入力言語のキーワードやその他の興味深いバイトシーケンス(例:マルチバイトマジック値)を含むユーザー提供の辞書をサポートしています。-dict=DICTIONARY_FILEを使用します。一部の入力言語では、辞書を使用すると検索速度が大幅に向上する場合があります。辞書の構文は、AFL-xオプションで使用されている構文に似ています。

# Lines starting with '#' and empty lines are ignored.

# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"

CMP命令のトレース

追加のコンパイラフラグ-fsanitize-coverage=trace-cmpを使用すると(-fsanitize=fuzzerの一部としてデフォルトで有効になっています。SanitizerCoverageTraceDataFlowを参照)、libFuzzerはCMP命令をインターセプトし、インターセプトされたCMP命令の引数に基づいて変異をガイドします。これによりファジングは遅くなる可能性がありますが、結果が改善される可能性が非常に高くなります。

値プロファイル

-fsanitize-coverage=trace-cmp-fsanitize=fuzzer を使用する場合のデフォルト)と追加の実行時フラグ -use_value_profile=1 を使用すると、ファザーは比較命令のパラメータの値プロファイルを集め、新しい値の一部を新しいカバレッジとして扱います。

現在の実装では、おおよそ次の処理を行います。

  • コンパイラは、CMP 命令すべてに、CMP の両方の引数を受け取るコールバックを挿入します。

  • コールバックは (caller_pc & 4095) | (popcnt(Arg1 ^ Arg2) << 12) を計算し、この値を使用してビットセット内のビットを設定します。

  • ビットセットで観測された新しいビットはすべて、新しいカバレッジとして扱われます。

この機能は、多くの興味深い入力を発見する可能性がありますが、2 つの欠点があります。まず、追加のインストルメンテーションにより、最大 2 倍の速度低下が発生する可能性があります。次に、コーパスが数倍に増加する可能性があります。

ファザーフレンドリーなビルドモード

テスト対象のコードがファジングに適していない場合があります。例:

  • テスト対象コードが、システム時間などでシードされた PRNG を使用しており、結果が同じでも、連続する呼び出しで異なるコードパスを実行する可能性があります。これにより、ファザーは 2 つの類似した入力を大きく異なるものとして扱い、テストコーパスが膨張します。たとえば、libxml はハッシュテーブル内で rand() を使用しています。

  • テスト対象コードが無効な入力を防ぐためにチェックサムを使用しています。たとえば、png はチャンクごとに CRC をチェックします。

多くの場合、特定のファジングに適さない機能を無効にした、特別なファザーフレンドリーなビルドを作成することが理にかなっています。一貫性を保つために、このようなすべてのケースで共通のビルドマクロ FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION を使用することを提案します。

void MyInitPRNG() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
  // In fuzzing mode the behavior of the code should be deterministic.
  srand(0);
#else
  srand(time(0));
#endif
}

AFLとの互換性

LibFuzzer は、同じテストコーパスで AFL と共に使用できます。どちらのファザーも、テストコーパスがディレクトリに存在し、入力ごとに 1 つのファイルがあることを期待します。どちらのファザーも、同じコーパスに対して連続して実行できます。

./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
./llvm-fuzz testcase_dir findings_dir  # Will write new tests to testcase_dir

定期的に両方のファザーを再起動して、互いの発見を活用できるようにします。現在、同じコーパスディレクトリを共有しながら、両方のファジングエンジンを並列に実行する簡単な方法はありません。

テスト対象関数 LLVMFuzzerTestOneInput に対して AFL を使用することもできます。例は こちら を参照してください。

ファザーの有効性

テスト対象関数 LLVMFuzzerTestOneInput を実装し、徹底的にファジングしたら、関数またはコーパスをさらに改善できるかどうかを知りたいと思うでしょう。使いやすい指標の 1 つは、もちろんコードカバレッジです。

Clang カバレッジ を使用して、コードカバレッジを視覚化および調査することをお勧めします()。

ユーザー提供のミューテーター

LibFuzzer はカスタム(ユーザー提供)ミューテーターの使用を許可します。詳細については、構造認識ファジング を参照してください。

スタートアップ初期化

テスト対象のライブラリを初期化する必要がある場合、いくつかのオプションがあります。

LLVMFuzzerTestOneInput 内(またはそれが機能する場合はグローバルスコープ内)に静的に初期化されたグローバルオブジェクトを使用するのが最も簡単な方法です。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  static bool Initialized = DoInitialization();
  ...

または、オプションの初期化関数を定義できます。この関数は、読み取りおよび変更できるプログラム引数を受け取ります。argv/argc にアクセスする必要がある場合にのみ、これを実行してください。

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {
 ReadAndMaybeModify(argc, argv);
 return 0;
}

ライブラリとしての libFuzzer の使用

ファジング対象のコードが独自の main を提供する必要がある場合、libFuzzer をライブラリとして呼び出すことができます。コンパイル時に -fsanitize=fuzzer-no-link を渡すこと、およびメインなしバージョンの libFuzzer に対してバイナリをリンクすることを確認してください。Linux インストールでは、通常は次の場所に配置されます。

/usr/lib/<llvm-version>/lib/clang/<clang-version>/lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

libFuzzer をソースからビルドする場合、これはビルド出力ディレクトリの次のパスにあります。

lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

ここから、コードは必要なセットアップを実行し、ファジングを開始する準備ができたら、プログラム引数とコールバックを渡して LLVMFuzzerRunDriver を呼び出すことができます。このコールバックは LLVMFuzzerTestOneInput と同様に呼び出され、同じシグネチャを持ちます。

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

不要な入力の拒否

一部の入力を拒否する、つまりコーパスに追加しないことが望ましい場合があります。

たとえば、解析とその他のロジックからなる API をファジングする場合、正常に解析された入力のみをコーパスに許可したい場合があります。

ファジングターゲットが特定の入力に対して -1 を返す場合、libFuzzer は、それがトリガーするカバレッジに関係なく、その入力をコーパスに追加しません。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  if (auto *Obj = ParseMe(Data, Size)) {
    Obj->DoSomethingInteresting();
    return 0;  // Accept. The input may be added to the corpus.
  }
  return -1;  // Reject; The input will not be added to the corpus.
}

リーク

AddressSanitizer または LeakSanitizer を使用してビルドされたバイナリは、プロセスのシャットダウン時にメモリリークを検出しようとします。プロセス内ファジングでは、ファザーはリーキーなミューテーションが見つかり次第、再現可能なリークを報告する必要があるため、これは不便です。ただし、ミューテーションごとに完全なリーク検出を実行するのはコストが高くなります。

デフォルトでは (-detect_leaks=1) libFuzzer は、ミューテーションごとに実行する mallocfree の呼び出し回数をカウントします。数が一致しない場合(それ自体がリークがあることを意味するわけではありません)、libFuzzer はより高価な LeakSanitizer パスを呼び出し、実際のリークが見つかった場合は、再現可能な情報と共に報告され、プロセスは終了します。

ターゲットに大量のリークがあり、リーク検出が無効になっている場合、最終的に RAM が不足します(-rss_limit_mb フラグを参照)。

libFuzzer の開発

LibFuzzer は、macOS と Linux ではデフォルトで LLVM プロジェクトの一部としてビルドされます。他のオペレーティングシステムのユーザーは、-DCOMPILER_RT_BUILD_LIBFUZZER=ON フラグを使用してコンパイルを明示的に要求できます。テストは、-DCOMPILER_RT_INCLUDE_TESTS=ON フラグで構成されたビルドディレクトリから check-fuzzer ターゲットを使用して実行されます。

ninja check-fuzzer

よくある質問

Q. libFuzzer はなぜ LLVM のサポートを使用しないのですか?

2 つの理由があります。

まず、このライブラリを、ユーザーが LLVM の残りの部分をビルドすることなく、LLVM の外部で使用できるようにしたいと考えています。多くの LLVM の関係者にとって、これは説得力がないように聞こえるかもしれませんが、実際には、LLVM 全体を作成する必要があるという事実は、多くの潜在的なユーザーを怖がらせます。そして、私たちはより多くのユーザーにこのコードを使用してもらいたいと考えています。

次に、LLVM の残りの部分、または他の大規模なコード(STL でさえもないかもしれません)に依存しないという微妙な技術的な理由があります。カバレッジインストルメンテーションが有効になっている場合、LLVM のサポートコードにもインストルメンテーションが行われ、プロセスのカバレッジセットが膨張します(ファザーはプロセス内にあるため)。つまり、外部の依存関係を増やすことで、ファザーの速度が低下します。ファザーが存在する主な理由は、極端な速度であるためです。

Q. libFuzzer は Windows をサポートしていますか?

はい、libFuzzer は現在 Windows をサポートしています。初期のサポートは r341082 で追加されました。Clang 9 のビルドであればすべてサポートしています。libFuzzer を含む Windows 用 Clang のビルドは、LLVM スナップショットビルド からダウンロードできます。

ASAN を使用せずに Windows で libFuzzer を使用することはサポートされていません。/MD(動的ランタイムライブラリ)コンパイルオプションを使用してファザーをビルドすることはサポートされていません。将来、これらに対するサポートが追加される可能性があります。/INCREMENTAL リンクオプション(またはそれを含む /DEBUG オプション)を使用してファザーをリンクすることもサポートされていません。

ご質問やご意見は、メーリングリスト libfuzzer(#)googlegroups.com までお寄せください。

Q. libFuzzer が問題に対する適切な解決策ではないのはいつですか?

  • テスト入力がターゲットライブラリによって検証され、バリデーターが無効な入力に対してアサートまたはクラッシュする場合、プロセス内ファジングは適用できません。

  • ターゲットライブラリのバグが検出されずに蓄積される可能性があります。たとえば、最初に検出されずに、別の入力をテストしている間にクラッシュを引き起こすメモリ破損などです。そのため、ほとんどのバグをその場で検出するために、すべてのサニタイザーを使用してこのプロセス内ファザーを実行することを強くお勧めします。

  • プロセス内ファザーを、ターゲットライブラリにおける過剰なメモリ消費と無限ループから保護することは困難です(それでも可能です)。

  • ターゲットライブラリは、実行間でリセットされない重要なグローバル状態を持たない必要があります。

  • 多くの興味深いターゲットライブラリは、プロセス内ファジングインターフェースをサポートするような設計になっていません(例:バイト配列ではなくファイルパスを必要とする)。

  • 単一のテスト実行にかなりの時間(0.1秒以上)がかかる場合、プロセス内ファザーによる速度向上効果は無視できます。

  • ターゲットライブラリが永続的なスレッド(1つのテストの実行よりも長く存続するスレッド)を実行する場合、ファジングの結果は信頼できません。

Q. それでは、このファザーは何に適しているのでしょうか?

このファザーは、比較的小さな入力を使用し、各入力の実行時間が10ms未満で、ライブラリコードが無効な入力でクラッシュしないことが予想されるライブラリのテストに適している可能性があります。例:正規表現マッチャー、テキストまたはバイナリ形式パーサー、圧縮、ネットワーク、暗号。

Q. LibFuzzerが複雑なファズターゲットでクラッシュする(しかし、小さなターゲットでは正常に動作する)。

ファズターゲットがdlcloseを使用しているかどうかを確認してください。現在、LibFuzzerはdlcloseを呼び出すターゲットをサポートしていません。これは将来修正される可能性があります。

成果