
PHPライブラリ向けのエッジカバレッジ誘導型ファザー。クラッシュ、タイムアウト、警告によってバグを検出します。コーパス管理、クラッシュの最小化、コードカバレッジレポートをサポートします。
このライブラリは、PHP用のファザーを実装しており、ライブラリ(特にパースライブラリ)に「ランダムな」入力を与えることでバグを発見するために使用できます。エッジカバレッジのインストルメンテーションからのフィードバックを使用して、新しいコードパスが実行されるように「ランダムな」入力を選択します。
Phar(推奨): このライブラリのPharパッケージはリリースページからダウンロードできます。Pharを使用することを推奨します。これは、PHP-Parserを使用するライブラリとの依存関係の競合を回避できるためです。
Composer: composer global require nikic/php-fuzzer
まず、ターゲット関数の定義が必要です。以下は、microsoft/tolerant-php-parserのバグを見つけるためのターゲットの例です。
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// 必須: ターゲットは単一の入力文字列を受け取り、それをテスト対象のライブラリで実行します。
// ターゲットは通常のException(無視される)をスローすることは許容されますが、
// Error例外は発見されたバグと見なされます。
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// オプション: 多くのターゲットでは、小さな入力でも生成できる大きな入力でバグが発生しません。
// 長さを制限するとパフォーマンスが向上する場合があります。
$config->setMaxLen(1024);
// オプション: 辞書を使用して、言語キーワードなどの有用な断片をファザーに提供できます。
// これは、例えばtoken_get_all()のようなインストルメント化されていないPHP拡張関数によって
// 処理されるため、ファザーが簡単に発見できない場合に特に重要です。
$config->addDictionary('example/php.dict');
ファザーは、初期の「興味深い」入力のコーパスに対して実行されます。これは、例えば既存の単体テストに基づいてシードできます。コーパスが指定されていない場合は、代わりに一時的なコーパスディレクトリが作成されます。
# 初期コーパスなしで実行
php-fuzzer fuzz target.php
# 初期コーパスを使用して実行(1ファイルにつき1入力)
php-fuzzer fuzz target.php corpus/
ファジングが中断された場合、後で同じコーパスディレクトリを指定して再開できます。
クラッシュが見つかると、crash-HASH.txtファイルに書き込まれます。これは元々見つかった形式で提供され、不必要に複雑でクラッシュに関係のない断片が含まれている可能性があります。そのため、最初にクラッシュ入力を削減することをお勧めします。
php-fuzzer minimize-crash target.php crash-HASH.txt
これにより、次第に小さくなるminimized-HASH.txtファイルのシーケンスが生成されます。クラッシュ入力に対して生成された例外トレースをすばやく確認したい場合は、run-singleコマンドを使用できます。
php-fuzzer run-single target.php minimized-HASH.txt
最後に、HTMLコードカバレッジレポートを生成することが可能です。これは、特定のコーパスからの入力を実行したときにターゲット内のどのコードブロックがヒットするかを示します。
php-fuzzer report-coverage target.php corpus/ coverage_dir/
追加の設定オプションは、php-fuzzer --helpで表示できます。
ファザーの実行中は、1行の出力を使用してステータスを継続的に報告します。この行は、次の順序で以下の部分で構成されています。
NEW または REDUCED: このステータス行をトリガーしたアクション。NEWは新しい入力がコーパスに追加されたことを示し、REDUCEDは既存のコーパスエントリがより短い入力に置き換えられたことを示します。run: N: ファザー起動以降に実行されたファジングイテレーション(ターゲット実行)の総数(N/s): 現在の実行速度(1秒あたりの実行回数)ft: N: これまでに発見されたユニークな特徴の総数(N/s): ファザー起動以降、1秒あたりに発見された新しい特徴の平均数corp: N: 現在コーパスに保存されている興味深い入力の数(%s): コーパス内の全入力の合計サイズlen: %d/%d: 最初の数字はアクションをトリガーした現在の入力の長さ(バイト単位)、2番目の数字は現在の最大許容入力長t: ファザー起動からの経過時間(秒)mem: PHPプロセスの現在のメモリ使用量ファザーはデフォルトで3種類のバグを検出します。
Error例外。Exception例外は不正な入力に対する通常の結果と見なされますが、キャッチされないError例外は常にプログラミングエラーを示します。これらは、例えばnullに対してメソッドを呼び出すときなど、PHP自体によって最も一般的に生成されます。Error例外に変換します。pcntl_alarm()と、タイムアウト時にErrorをスローする非同期シグナルハンドラを使用して実現されます。特に、これらのいずれもターゲットの出力が正しいかどうかをチェックしません。ターゲットが極端に誤動作していないことだけを判断します。出力の正しさをチェックする1つの方法は、同一の結果を生成するはずの2つの異なる実装を比較することです。
$fuzzer->setTarget(function(string $input) use($parser1, $parser2) {
$result1 = $parser1->parse($input);
$result2 = $parser2->parse($input);
if ($result1 != $result2) {
throw new Error('Results do not match!');
}
});
このファザーの技術的詳細の多くは、LLVMプロジェクトのlibFuzzerに基づいています。以下に、実装の詳細の一部を説明します。
効率的に動作するために、ファジングには特定のファジング入力をテスト中に実行されたコードパスに関するフィードバックが必要です。このカバレッジフィードバックは、ファジングターゲットを「インストルメント化」することによって収集されます。include-interceptorライブラリを使用して、インクルードされたすべてのファイルのコードをオンザフライで変換します。PHP-Parserライブラリを使用してコードを解析し、追加のインストルメンテーションコードを挿入する必要があるすべての場所を見つけます。
すべての基本ブロック内に、次のコードが挿入されます。ここでBLOCK_INDEXはブロックごとに一意の整数です。
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;
これは、ブロックインデックスが最大28ビットであると仮定し、実行中に観測された(prev_block, cur_block)ペアの数をカウントします。生成されたコードは、初期化されていないエッジカウントを処理する必要性と静的プロパティの使用により、残念ながらかなり高価です。将来的には、カバレッジフィードバックをより効率的に収集できるPHP拡張を作成することが可能でしょう。
場合によっては、基本ブロックが式の一部であるため、コードを簡単に挿入できません。そのような場合は、代わりに上記のコードを含むメソッドへの呼び出しを挿入します。
if ($foo && $bar) { ... }
// 変換後
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
将来的には、比較もインストルメント化することで、$foo == "SOME_STRING"のような比較から辞書エントリを自動的に決定できるようにすることが有益です。
ファジング入力は、既にコーパスの一部である他の入力で観測されていない新しい特徴を含む場合に「興味深い」と見なされます。このライブラリは、粗粒度のエッジヒットカウントを特徴として使用します。
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
近似ヒットカウントは、実際のヒットカウントを8つのカテゴリ(AFLに基づく)に減らします。
0: 0ヒット
1: 1ヒット
2: 2ヒット
3: 3ヒット
4: 4-7ヒット
5: 8-15ヒット
6: 16-127ヒット
7: >=128ヒット
このように、各入力は特徴を表す整数のセットに関連付けられます。さらに、入力がテストされた時点で他のコーパス入力では見られない「ユニークな特徴」のセットを持ちます。
入力にユニークな特徴がある場合、それはコーパスに追加されます(NEW)。入力Bが入力Aの変異によって作成され、入力Bがより短く、入力Aのすべてのユニークな特徴を持っている場合、Aはコーパス内でBに置き換えられます(REDUCE)。
各イテレーションで、現在のコーパスからランダムな入力が選択され、一連の変異器を使用して変異されます。現在、以下の変異器(libFuzzerから取得)が実装されています。
EraseBytes: いくつかのバイトを削除します。InsertByte: 新しいランダムなバイトを挿入します。InsertRepeatedBytes: ランダムなバイトを複数回繰り返して挿入します。ChangeByte: バイトをランダムなバイトで置き換えます。ChangeBit: 1ビットを反転します。ShuffleBytes: 小さな部分文字列をシャッフルします。ChangeASCIIInt: ASCII整数をインクリメント/デクリメント/倍加/半減させます。ChangeBinInt: バイナリ整数を少量のランダムな量だけ変更します。CopyPart: 文字列の一部を別の部分にコピーします(上書きまたは挿入)。CrossOver: 複数の戦略で別のコーパスエントリと交叉させます。AddWordFromManualDictionary: 辞書(存在する場合)から単語を挿入または上書きします。変異は最大長の制約を受けます。全体的な最大長はターゲットで指定できますが(setMaxLength())、ファザーは自動的な長さ制御も行います(--len-control-factor)。最大長は最初は非常に低い値に設定され、最後のlen_control_factor * log(maxlen)回の実行でアクション(NEWまたはREDUCE)が行われなかった場合にlog(maxlen)ずつ増加します。
長さ制御係数が高いほど、ファザーは長い入力を許可する前に短い入力をより積極的に探索します。これにより、生成されるコーパスのサイズが大幅に削減されますが、初期の探索は遅くなります。