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をソースからビルドしていきます。ここでは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上で以下のコマンドを実行します。
$ 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上で以下のコマンドを実行します。
$ 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
で下記ソースコードをコンパイルします。
# 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にはthisisstr
とend
が出力されます。
14abcd09thisisstr03end
2. オプション”-target_offset”の確認
WinAFL実行時のtarget functionのoffsetを確認します。
やり方はたくさんあると思いますが、ここではIDA proで今回のターゲット関数であるmain関数のoffsetを確認します。
main関数が0x401537で始まっているのでBase addressが0x400000、offsetが0x1537であると分かります。
3. WinAFLの実行
afl-fuzz.exeが置いてあるディレクトリと同じディレクトリに以下の2つを配置します。
- txt_parser.exe
- testcases\input.txt
次に以下のコマンドを実行します。
$ 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エラーが出てしまうのを防ぐためです。
Fuzzing実行中には以下の画面が表示され、進捗を確認することができます。
exec speedがzzzz…と表示されておりFuzzingの効率は悪い状態ですが、今回は良しとしましょう。
unique crashが4つ見つかっているのでcrashesフォルダを確認すると、crashを引き起こすインプットファイルが保存されています。
これらのファイルを入力することでcrashを簡単に再現させることもできます。
各項目が意味する内容について、AFL User Guideを参考に、上の画面内に赤字で付与した(1)~(6)の順で以下に概要を記載していきます。
WinAFLステータス画面
(1) process timing
Fuzzingを始めてからの経過時間と、最後に新しいpath, crach, hangを発見してからの経過時間が表示されています。
ほとんどのFuzzing jobは数日~数週間かかることが予測されます。
Fuzzingを始めてから数分経過しても新しいpathが見つからない場合には、下記3つの原因が考えられます。
- ターゲットバイナリが正しく呼び出されておらず、インプットファイルがパースされていない
- デフォルトのメモリ制限(-m)が厳しすぎ、メモリの割り当てに失敗後すぐにプログラムが終了している
- インプットファイルが明らかに不正なフォーマットであり、基本的なヘッダチェックの時点で全て失敗している
(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つの可能性が考えられます。
- プログラムが極端にシンプル
- 適切にDBIが機能していない
- 早期にターゲットの実行が終了してしまう
(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使用率。