FormatFuzzerは、バイナリ入力を高効率かつ高品質に生成・解析するためのフレームワークです。
バイナリ入力の形式を記述したバイナリテンプレートを受け取り、指定されたバイナリ形式を生成・解析する実行可能ファイルを生成します。
例えば、GIF用のバイナリテンプレートから、FormatFuzzerはGIFジェネレータ(別名GIFファザー)を生成します。
FormatFuzzerが生成するジェネレータは非常に効率的で、毎秒数千もの有効なテスト入力を生成します。これはミューテーションベースのファザーとは対照的で、ミューテーションベースでは入力の大部分が無効です。FormatFuzzerが生成する入力は、テスト対象プログラム(あるいは実際には任意のプログラム)とは独立しているため、ブラックボックス設定でも使用できます。さらに、FormatFuzzerはAFL++と統合して、最大カバレッジを目指す有効な入力を生成することもできます。私たちの実験では、この「二つの世界のベスト」アプローチが他のすべての設定を上回りました。詳細は私たちの論文をご覧ください。
FormatFuzzerが使用するバイナリテンプレートは、010 editorから提供されています。
170以上のバイナリテンプレートがあり、FormatFuzzerで直接使用するか、またはその用途に合わせて調整することができます。すぐに使える状態で、FormatFuzzerはAVI、BMP、GIF、JPG、MIDI、MP3、MP4、PCAP、PNG、WAV、ZIPなどの形式を生成します。そして、毎週このリストを拡張し続けています。
コントリビューターを歓迎します! FormatFuzzerプロジェクトページにアクセスして、アイデアや問題を報告したり、プルリクエストを追加してください。FormatFuzzerの動作方法や比較については、私たちの論文をご覧ください。
FormatFuzzerは、FormatFuzzerプロジェクトページから入手できます。最新リリースはリリースページからダウンロードして解凍できます。
最新かつ最高のものをお求めの場合は、Gitリポジトリをクローンすることもできます:
git clone https://github.com/uds-se/FormatFuzzer.git
以降の操作はすべてメインフォルダで行います:
cd FormatFuzzer
FormatFuzzerを実行するには、以下が必要です:
getopt_long())を備えたC++コンパイラ(clangやgccなど)py010parser、six、intervaltreezlibライブラリ(圧縮関数用)boostライブラリ(チェックサム関数用)ビルドおよび設定スクリプト(.ac および .am ファイル)を編集する場合は、以下も必要です:
sudo apt install git g++ make automake python3-full zlib1g-dev libboost-dev
python3 -m venv ~/fuzz
source ~/fuzz/bin/activate
pip3 install py010parser six intervaltree
xcode-select --install
brew install python3 automake boost
pip3 install py010parser six intervaltree
すべてのシステムで、pipを使用:
pip install py010parser
pip install six
pip install intervaltree
注意:すべてのビルドコマンドは、このREADMEファイルと同じフォルダにいる必要があります。このフォルダの外でファザーをビルドすることはまだサポートされていません。
すべての構築手順を自動化するbuild.shスクリプトがあります。
単に以下を実行してください:
./build.sh gif
これでGIFファザーが作成されます。
これはtemplates/内にあるすべてのファイル形式で機能します。templates/FOO.btというファイルがあれば、./build.sh FOOでファザーがビルドされます。
すべての構築手順を自動化するMakefile(ソースはMakefile.am)があります。
(GNU makeが必要です。)
最初に以下を実行します:
touch configure Makefile.in
次に:
./configure
そして:
make gif-fuzzer
これでGIFファザーが作成されます。
これはtemplates/内にあるすべてのファイル形式で機能します。templates/FOO.btというファイルがあれば、make FOO-fuzzerでファザーがビルドされます。
上記のmake方法が機能しない場合、またはより細かい制御が必要な場合は、手動で進める必要があるかもしれません。
ffcompileコンパイラを実行して、バイナリテンプレートをC++コードにコンパイルします。2つの引数を取ります:.btバイナリテンプレートと、生成される.cpp C++ファイル。
./ffcompile templates/gif.bt gif.cpp
以下のコマンドを使用して、ファザーgif-fuzzerを作成します。
まず、汎用コマンドラインドライバをコンパイルします:
g++ -c -I . -std=c++17 -g -O3 -Wall fuzzer.cpp
(-I .はbt.hファイルの場所を示します。-std=c++17はC++標準を設定します。)
次に、バイナリパーサー/コンパイラをコンパイルします:
g++ -c -I . -std=c++17 -g -O3 -Wall gif.cpp
最後に、バイナリパーサー/コンパイラをコマンドラインドライバとリンクして実行可能ファイルを取得します。追加のライブラリ(-lzなど)を使用する場合は、ここでそれらも指定してください。
g++ -O3 gif.o fuzzer.o -o gif-fuzzer -lz
FormatFuzzerは、特定の形式のスタンドアロンパーサー、ジェネレーター、またはミューテーターとして実行できます。 さらに、汎用ファザー(AFL++など)から呼び出して、それらの形式固有の機能をファジングプロセスに統合することもできます(下記のAFL++統合のセクションを参照)。
生成されたファザーは、最初の引数としてコマンドを取り、その後にそのコマンドのオプションと引数が続きます。
最も重要なコマンドはfuzzで、出力を生成します。その引数は、適切な形式で生成されるファイルです。
ジェネレーターを次のように実行します:
./gif-fuzzer fuzz output.gif
これにより、ランダムなバイナリファイルoutput.gifが作成されます。あるいは:
./gif-fuzzer fuzz out1.gif out2.gif out3.gif
これにより、3つのGIFファイルout1.gif、out2.gif、out3.gifが作成されます。
提供されているgif.btテンプレートは、有効なファイルの生成を容易にするための特別な関数で拡張されています。元の.btテンプレートファイルをそのまま使用する場合、生成中に警告が表示されたり、無効なファイルが作成されたりする可能性があります。
parseコマンドを使用して、ファザーをバイナリファイルのパーサーとして実行することもできます。これは、バイナリテンプレートの正確性をテストしたり、入力をミューテーションしたい場合に便利です(下記の「決定ファイル」を参照)。
パーサーを実行するには、次を使用します:
./gif-fuzzer parse input.gif
input.gifが正常に解析できない場合は、エラーメッセージが表示されます。
解析中に、すべての解析決定(つまり、どの解析代替が選択されたか)を決定ファイルに保存することもできます。これは、行われた決定を列挙するバイトのシーケンスです。
各バイトは1つの解析決定を表します。バイト値0は最初の代替が選択されたことを意味し、バイト値1は2番目の代替が選択されたことを意味し、以下同様です。
入力を解析するときに、そのような決定ファイルを生成できます:
./gif-fuzzer parse --decisions input.dec input.gif
ここで、input.decはinput.gifを解析するために行われた決定を保存します。
また、生成時にもそのような決定ファイルを使用できます。ファザーは、解析時に見つかったものとまったく同じ決定を下します。次のコマンドは、input.gifの解析中に決定された決定を使用して新しいGIFファイルを生成します:
./gif-fuzzer fuzz --decisions input.dec input2.gif
すべてがうまくいけば、両方のファイルは同一になるはずです:
cmp input.gif input2.gif
決定ファイルをミューテーション(例えば、個々のバイトを置き換える)することで、解析された元のファイルと類似した入力を作成できます。これは、特定のテスト戦略やAFLなどのファザーと連携するのに便利です。gif-fuzzerなどをトランスレーターとして使用し、決定ファイルをバイナリファイルに変換したり、その逆を行ったりできます。AFLは決定ファイルをミューテーションし、テスト対象プログラムは変換されたバイナリファイルで実行されます。バイナリファイルを直接ミューテーションする(AFLが通常行う方法)のとは対照的に、これにより常に有効な入力が得られるという利点があり、カバレッジに向けてより迅速に進むことができます。
gif-fuzzerのような形式固有のファザーに加えて、FormatFuzzerは形式固有の共有ライブラリ(gif.soなど)にコンパイルすることもできます(その場合は、単に./build.sh gifまたはmake gif.soを実行してください)。
これらの共有ライブラリは、AFL++などの汎用ファザーによってロードできます。
AFL++をFormatFuzzerと一緒に実行するには、私たちの修正版AFL++の指示に従ってください。 以下のようなさまざまなファジング戦略をサポートしています:
AFL+FFMut: FormatFuzzerを使用して形式固有のスマートミューテーションを提供しながらAFL++を実行します。
AFL+FFGen: FormatFuzzerを形式固有のジェネレーターとして使用し、AFL++がその決定シードをミューテーションします。
独自の.btバイナリテンプレートを作成して(これにより、その形式の高効率ファザー/パーサーを作成します)、010 Editorマニュアルのテンプレートとスクリプトの概要のセクションをお読みください。
多くの場合、探している形式(または類似の形式)のテンプレートがすでに存在する可能性があります。010 editorのバイナリテンプレートコレクションをチェックして、使用できるものやベースにできるものがあるかどうかを確認してください。
リポジトリに提供されている.btファイルは、一般的にファイルの解析を対象としています。生成にも使用できますが、多くの場合、入力のどの部分が必要かについての正確な情報が不足しています。
このセクションでは、.btファイルをFormatFuzzerでうまく動作するようにカスタマイズする方法のいくつかを説明します。
たとえば、GIF形式の場合、ファイルtemplates/gif-orig.btは元のバイナリテンプレートを示しており、解析のみを目的として設計されています。一方、templates/gif.btは、有効なGIFを生成できるように修正されたバージョンです。2つのファイルを比較すると、これを達成するために必要な変更はごくわずかであることがわかります。
make gif-fuzzerを実行するか、ffcompileツールを使用してgif-fuzzerを作成した場合、GIFジェネレーターとパーサーの実装を含むC++ファイルgif.cppがすでに取得されています。これは、バイナリテンプレートに加えた変更がどのように実行可能コードに変換されるかを確認するのに役立ちます。C++コードの詳細については、次のセクションで説明します。
GIFバイナリテンプレートは、実際に構造体フィールドに解析する前に、ファイル内の次のバイトの値を先読みするために、先読み関数ReadUByte()とReadUShort()を使用します。生成時には、これらの関数が追加の引数を受け取り、先読みするバイトに選択する既知の適切な値のセットを指定できるようにしています。さらに、ReadUByte()などの特定の先読み関数を呼び出すときに常に使用する、既知の適切な値のグローバルセットを指定することもできます。これらはReadUByteInitValuesベクターに保存されます。
デフォルトでは、私たちの変換手順ffcompileは、先読みバイトとの比較に使用された興味深い値をマイニングし、それらを既知の値のグローバルセットとして使用しようとします。./ffcompile templates/gif.bt gif.cppを実行すると、識別された先読み関数とマイニングされた興味深い値が表示されます:
Finished creating cpp generator.
Lookahead functions found:
ReadUByte
ReadUShort
Mined interesting values:
GlobalColorTableFlag: ['1']
LocalColorTableFlag: ['1']
ReadUByte: ['0x3B', '0x2C']
ReadUShort: ['0xF921', '0xFE21', '0x0121', '0xFF21']
Signature: ['"GIF"']
ただし、GIF生成では、ReadUByte()の呼び出しごとに個別に既知の適切な値のセットを指定する方が適切です。そのため、サイズ0の空の配列を定義します:
const local UBYTE ReadUByteInitValues[0];
これにより、グローバルなReadUByteInitValuesのセットが上書きされ、ReadUByte()の呼び出しごとに、その特定の場所で使用する適切な値のセットを指定する追加の引数を使用します。
バイナリテンプレート言語は、この選択を実行時条件に基づいて行うこともできるほど強力です。たとえば、次のコードでは、ReadUByte()呼び出しの適切な値の選択が、生成している現在のGIFバージョンに依存する方法を示しています。GIFバージョン89aでは、バイトに対して追加の可能な値(0x21)が1つ許可されます。
if(GifHeader.Version == "89a")
local UBYTE values[] = { 0x3B, 0x2C, 0x21 };
else
local UBYTE values[] = { 0x3B, 0x2C };
while (ReadUByte(FTell(), values) != 0x3B) {
...
}
GIFバイナリテンプレートに必要な残りの編集も同様です。たとえば、各構造体フィールドに対して、既知の適切な値のセットを指定することもできます。次の例は、Versionフィールドの正しい値(87aおよび89a)を指定しています。
char Version[3] = { {"87a"}, {"89a"} };
デバッグ目的、およびジェネレーターとパーサーを改善するための適切な変更方法を理解するために、生成されたC++コードの内部動作を理解することは役立つかもしれません。 理想的には、バイナリテンプレートファイルを編集して、高い確率で有効なファイルを生成できるようにするだけで、生成されたC++コードを編集する必要はないでしょう。
C++コードは、バイナリテンプレートで定義された各structおよびunion、さらにintなどのネイティブ型に対してクラスを作成します。
変数を初期化する構築時には、その変数が取り得る既知の適切な値のセットを定義できます。たとえば、コンストラクタ呼び出し:
char_array_class cname(cname_element, { "IHDR", "tEXt", "PLTE", "cHRM", "sRGB", "iEXt", "zEXt", "tIME", "pHYs", "bKGD", "sBIT", "sPLT", "acTL", "fcTL", "fdAT", "IHDR", "IEND" });
は、変数cnameに使用する17個の適切な値を指定します。ただし、適切なチャンクタイプの選択は文脈に依存するため、これだけでは不十分なことがよくあります。
そのため、新しいチャンクを生成する際に、生成時に適切な値のセットを指定することもできます。たとえば、最初のチャンク(タイプがIHDRでなければならない)のchunkインスタンスを生成するために、次の呼び出しを使用できます:
GENERATE(chunk, ::g->chunk.generate({ "IHDR" }, false));
2番目のチャンクを生成するときは、IHDRチャンクとPLTEチャンクの間に現れる可能性のあるチャンクの長いリストを使用するかもしれません:
GENERATE(chunk, ::g->chunk.generate({ "iCCP", "sRGB", "sBIT", "gAMA", "cHRM", "pHYs", "sPLT", "tIME", "zTXt", "tEXt", "iTXt", "eXIf", "oFFs", "pCAL", "sCAL", "acTL", "fcTL", "fdAT", "fRAc", "gIFg", "gIFt", "gIFx", "sTER" }, true));
ジェネレーターは、新しいインスタンスに使用する既知の適切な値から均一に1つを選択します。また、小さな確率1/128で、既知の適切な値ではない「悪意のある」値を選択することもできます。
この機能は、set_evil_bitメソッドを使用していつでも有効または無効にできます。
ジェネレーターが取るすべてのランダムな選択は、rand_int()メソッドを呼び出すことによって行われます。
long long rand_int(unsigned long long x, std::function<long long (unsigned char*)> parse);
プログラムをジェネレーターとして実行する場合、このメソッドはランダムバッファからバイトを読み取ることで、0からx-1までの整数をサンプリングします。
プログラムをパーサーとして実行する場合、このメソッドはparse()関数を使用して、ターゲットファイルを生成するためにランダムバッファにどのランダムバイトが存在しなければならないかを見つけ、それらのバイトをランダムバッファに書き込みます。
parse関数は、ファイルの現在位置のバッファを引数として受け取り、この正確なファイル構成を生成するために、現在のrand_int()呼び出しによって返されなければならない値を返す必要があります。
FormatFuzzerは、Rafael Dutra <[email protected]> によって設計・作成されました。
ファザーコンパイラの概念は、Rahul Gopinath <[email protected]> とAndreas Zeller <[email protected]> によって導入されました。
FormatFuzzerは、Copyright © 2020, 2021 by CISPA Helmholtz Center for Information Securityです。以下のライセンスが適用されます:
FormatFuzzerコード(特に、すべてのC++コードおよびその生成に関連するコード)は、COPYINGに記載されているGNU GENERAL PUBLIC LICENSEの対象となります。
上記の例外として、FormatFuzzerによって生成されたC++コード(つまり、特定の形式のファザーとパーサー)はパブリックドメインです。
FormatFuzzerのベースとなっている_元の_ pfp _コード_は、LICENSE-pfpに記載されているMITライセンスの対象となります。