← All Articles

WinAFLで始めるGreybox fuzzing

Posted on

WinAFLとは


WinAFLは、AFL (American Fuzzy Lop)のWindows向けに開発されたFolkであり、Greybox fuzzerの一種です。Greybox fuzzerはCoverage-guided fuzzerとも呼ばれます。WinAFLとDynamoRIO等のDBIを合わせて利用することで、カバレッジを確認しながら新しい実行パスを探索することができます。

WinAFLの画面


WinAFLを使ってみる


以下の手順に沿ってWinAFLをソースからビルドしていきます。ここでは32-bitと64-bitどちらも合わせてビルドします。

1. DynamoRIOをビルド

詳細な手順はDynamoRIOで始めるDBIにて紹介しています。


2. WinAFL (32-bit) をビルド

スタートメニュー > すべてのアプリケーション > Visual Studio 2017 > x86 Native Tools Command Promptを開きます。

x86 Native Tools Command Prompt画面


x86 Native Tools Command Prompt上で以下のコマンドを実行します。

WinAFL(32-bit)ビルドコマンド
$ cd C:\
$ git clone https://github.com/googleprojectzero/winafl
$ cd winafl && mkdir build32 && cd build32
$ cmake -G"Visual Studio 15 2017" .. -DDynamoRIO_DIR=..\path\to\DynamoRIO\cmake
$ cmake --build . --config Release

32-bit WinAFLのビルドが完了しました。続いて64-bit WinAFLをビルドします。


3. WinAFL (64-bit) のビルド

スタートメニュー > すべてのアプリケーション > Visual Studio 2017 > x64 Native Tools Command Promptを開きます。


x64 Native Tools Command Prompt上で以下のコマンドを実行します。

WinAFL(64-bit)ビルドコマンド
$ cd C:\winafl && mkdir build64 && cd build64
$ cmake -G"Visual Studio 15 2017 Win64" .. -DDynamoRIO_DIR=..\path\to\DynamoRIO\cmake
$ cmake --build . --config Release

64-bit WinAFLのビルドが完了しました。


WinAFLを使ってみる


今回は、テキストファイルを読み取って動作する脆弱なサンプルバイナリをターゲットとします。

1. ターゲットバイナリを用意

gcc -o txt_parser txt_parser.c -m32で下記ソースコードをコンパイルします。

txt_parser.c
# include <windows.h>
# include <stdio.h>

void parse_text(FILE *input, FILE *output) {
    FILE *stream;
    char *buf;
    char ch;
    int offset = 0;
    int size = 0;
    int counter = 0;

    while((ch = fgetc(input)) != EOF) {
        if (offset == 0) {
            switch (ch) {
            case '1':
                stream = stdout;
                break;
            case '0':
                stream = output;
                break;
            default:
                perror("parse error");
                return;
            }
            ++offset;
            continue;
        }

        if (offset == 1) {
            size = (int) ch - 47;
            buf = malloc(size);
            memset(buf, '\0', size);
            counter = size;
            ++offset;
            continue;
        }

        if (counter > 2) {
            buf[size - counter] = ch;
            --counter;
            ++offset;
        } 
        else {
            buf[size - counter] = ch;
            fprintf(stream, "%s\n", buf);
            size = 0;
            counter = 0;
            offset = 0;
        }
    }
    return;
}

int main(int argc, char *argv[]) {
    FILE *input;
    FILE *output;

    puts("Start");

    input = fopen(argv[1], "r");
    if (input == NULL) {
        perror("Failed to fopen input file\n");
        return -1;
    }
    output = fopen(argv[2], "w");
    if (output == NULL) {
        perror("Failed to fopen output file\n");
        return -1;
    }

    parse_text(input, output);

    fclose(input);
    fclose(output);

    puts("Done");
    return 0;
}

上記ソースコードは下記の動作をします。

  • 1文字目が1の場合はstdout, 0の場合はarg[2]で指定したファイルへ出力
  • 2文字目が出力するテキストのサイズ指定
  • 3文字目から指定サイズ文の文字列を出力
  • 1文字目から再カウント

下記input.txtを引数にtxt_parser input.txt output.txtを実行した場合、標準出力にはabcd、output.txtにはthisisstrendが出力されます。

input.txt
14abcd09thisisstr03end

2. オプション”-target_offset”の確認

WinAFL実行時のtarget functionのoffsetを確認します。

やり方はたくさんあると思いますが、ここではIDA proで今回のターゲット関数であるmain関数のoffsetを確認します。

main関数の先頭アドレスの図

main関数が0x401537で始まっているのでBase addressが0x400000、offsetが0x1537であると分かります。


3. WinAFLの実行

afl-fuzz.exeが置いてあるディレクトリと同じディレクトリに以下の2つを配置します。

  • txt_parser.exe
  • testcases\input.txt

次に以下のコマンドを実行します。

WinAFL実行
$ cd C:\winafl\build32\bin\Release
$ afl-fuzz.exe -i testcases -o results -D C:\dynamorio\build\bin32 -t 10000+ -- -target_module txt_parser.exe -target_offset 0x1537 -coverage_module txt_parser.exe -nargs 3 -- txt_parser.exe @@ output.txt

※ afl-fuzz.exeと同じディレクトリにターゲットバイナリを置いたのはafl-fuzz.exe実行時に下記のUnable to load client libraryエラーが出てしまうのを防ぐためです。

Unable to load client libraryエラー


Fuzzing実行中には以下の画面が表示され、進捗を確認することができます。

WinAFLのステータス画面

exec speedがzzzz…と表示されておりFuzzingの効率は悪い状態ですが、今回は良しとしましょう。

unique crashが4つ見つかっているのでcrashesフォルダを確認すると、crashを引き起こすインプットファイルが保存されています。

これらのファイルを入力することでcrashを簡単に再現させることもできます。

crashesファイル

各項目が意味する内容について、AFL User Guideを参考に、上の画面内に赤字で付与した(1)~(6)の順で以下に概要を記載していきます。


WinAFLステータス画面


(1) process timing

Fuzzingを始めてからの経過時間と、最後に新しいpath, crach, hangを発見してからの経過時間が表示されています。

ほとんどのFuzzing jobは数日~数週間かかることが予測されます。

Fuzzingを始めてから数分経過しても新しいpathが見つからない場合には、下記3つの原因が考えられます。

  1. ターゲットバイナリが正しく呼び出されておらず、インプットファイルがパースされていない
  2. デフォルトのメモリ制限(-m)が厳しすぎ、メモリの割り当てに失敗後すぐにプログラムが終了している
  3. インプットファイルが明らかに不正なフォーマットであり、基本的なヘッダチェックの時点で全て失敗している

(2) Overall results

cycles doneは、一連のテストケース群であるサイクルが完了した回数を意味します。

Ctrl+Cでファジングを終了するまでには、最低1サイクルが完了するのを待つことが望ましいです。さらに言えば、新しいpath, crash, hangが見つからなくなるまで継続できるとよいです。


(3) Cycle progress

now processingは、処理中のテストケースIDと進捗を意味します。


(4) Map coverage

map densityは、カバレッジを意味します。左側は現在のテストケースにおけるカバレッジ、右側はテストケース全体のカバレッジを示しています。

count coverageが200以下の場合には以下の3つの可能性が考えられます。

  1. プログラムが極端にシンプル
  2. 適切にDBIが機能していない
  3. 早期にターゲットの実行が終了してしまう

(5) Stage progress

現在WinAFLが行っている動作をステージという表現で示しています。ステージには以下の9種類が存在します。

ステージ 説明
calibration Fuzzing実行前に、実行パスを調べて以上を検知したり実行速度ベースラインを確立したりします。新しい発見(?)のたびに短時間で実行されます。
trim L/S Fuzzing実行前に、同じ実行パスを生成する最も短い状態になるようテストケースが調整される。(LはLength、SはStepover)
bitflip L/S ビット反転。
arith L/8 加算または減算。
interest L/8 値の上書き。既知のinterestingな値のリストを用います。
extras 辞書からの単語注入。
havoc 上記2~6のテクニックが固定長で積み重ねられた状態。
splice 任意の中間点で2つのキューつなぎ合わせるということを除いてhavocと同じ。
sync 別のFuzzerからテストケースをインポート。-Mまたは-Sオプションで並列Fuzzingを実行するときのみ使われる。

各ステージ間の処理の流れは別の方のブログ記事 American Fuzzy Lop (AFL)の構造 に分かりやすくまとめてありました。


exec speedは500 exec/sec以上が理想的です。もし100 exec/sec以下になってしまう場合にはAFLのPerformance Tipsを参考にしてください。


(6) Findings in depth

Favored pathsは、Fuzzerが最も好むパスの数を意味します。


(7) Fuzzing strategy yields

Stage progressで説明したFuzzing戦略毎に発見した実行パスを表示します。


(8) Path geometry

stabilityは、同じインプットに対して毎回同じ振る舞いをした場合に100%となる。


(9) CPU load

論理コアの数と実行可能状態にあるプロセス数との比較で算出される見かけ上のCPU使用率。


reversingfuzzingwinafl