コンテンツにスキップ

Mayhem ターゲットの最適化

ファジングまたはファズ テストとは、プログラムにランダム化された入力 (テスト ケースと呼ばれます) を与えてプログラムをクラッシュさせようと試み、潜在的な脆弱性を検出する自動化されたアプリケーションテスト技法です。Mayhem の解析スイートに含まれるようなファザーは、さまざまなテスト ケースを使用して継続的にプログラムの実行を繰り返し、プログラムの動作のユニークな変化を観察して、プログラムを構成するコードのできるだけ広い範囲をカバーしようとします。

そのため、ファザーの効率は、主に 2 つの要素に依存します。プログラムの実行スピードと、ファザーがコードに存在する新しいパスをどれだけすばやくカバーできるかという有効性です。

高速な実行

ファザーは、それまでに発生していないプログラムの動作やコード パスを発見するため、実行のたびに入力を変化させながら、少なくとも何千回もプログラムを実行できます。しかし、プログラムの実行に時間がかかりすぎると、ファザーがプログラムをテストして新しいコード パスを発見するのにかかる時間 (および全体的な効率) に悪影響を与える可能性があります。

従って、ファズ ターゲットとなるプログラムの実行スピードを上げるため、プログラム自体の実行を遅らせる原因となる余計な処理があれば排除するべきです。これには、処理が完了するまで後続の部分を遅らせたりブロックしたりする sleep 関数や wait 関数、ディスク/ネットワーク IO などが含まれます。さらに好都合なことに、テスト ドライバーを作成すると、プログラム内の特定のコード領域により直接的にフォーカスし、余計な処理を最小限にして、ファズ ターゲットの実行スピードと新しいコードのカバレッジ率を上げることができます。

Tip

ファズ ターゲットの実行スピードを上げる方法の詳細については、「テスト実行率が低い場合の診断」を参照してください。

効果的なコード カバレッジ

ファズ ターゲットのコード カバレッジの有効性を最大化するため、プログラムは高速に実行できるだけでなく (より高速なプログラムはより高速なテストにつながり、結果としてより早くコード カバレッジが上がるため)、小さなテスト ケースとともにシード化されたテスト ケースを使用するべきです。これは、それぞれテスト スイートのシード化とテスト ケースの最小化と呼ばれます。

テスト スイートのシード化

可能であれば、シード テスト スイートと呼ばれるテスト ケースの初期セットを作成し、ファズ ターゲットに開始点を与えることは常に有益です。シード テスト スイートが用意されていない場合、ファジング エンジンはゼロから入力を推測する必要があります。入力のサイズやターゲットの複雑度によっては、これは時間がかかる場合があります。多くのケースで、シード テスト スイートを用意すると、コード カバレッジが飛躍的に向上する可能性があります。

Tip

テスト スイートのシード化の詳細については、「Mayhem での非 Docker ターゲットのテスト」のチュートリアルを参照してください。

テスト スイートの最小化

テスト スイートをシード化する際、初期テスト ケースがどれほど効果的にターゲット アプリケーションの実質的なコード カバレッジを最大化できるかを考えることも重要です。たとえば、開始点となるテスト スイートに含まれるテスト ケースが互いに似ていると、ターゲット アプリケーションの実質的なカバレッジは小さくなります。

従って、Mayhem は、テスト ケースごとにターゲットを繰り返し実行する必要がある大量の類似したテスト ケースを使用するのではなく、デフォルトでテスト スイートの最小化タスクを実行します。各初期テスト ケースのコード カバレッジを解析し、「最小セット」アルゴリズムによって最大の「投資の見返り」を得られるテスト ケースを特定します。言い換えると、Mayhem は開始点となるテスト スイートを最小化し、ターゲット アプリケーションの最も広い範囲をカバーするテスト ケースに限定します。

基礎的な技術サンプルを使って、このことを説明します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <string.h>

int testme(char *buf, unsigned len);

int main(int argc, char *argv[])
{
  FILE *f;
  char buf[12];

  if(argc != 2){
    fprintf(stderr, "Must supply a text file\n");
    return -1;
  }
  f = fopen(argv[1], "r");
  if(f == NULL){
    fprintf(stderr, "Could not open %s\n", argv[1]);
    return -1;
  }
  if(fgets(buf, sizeof(buf), f) == NULL){
    fprintf(stderr, "Could not read from %s\n", argv[1]);
    return -1;
  }
  testme(buf, strlen(buf));
  return 0;
}

int testme(char *buf, unsigned len)
{
  unsigned ok;

  if(!ok) // Defect: uninitialized use of ok.
    ok = len;

  if(buf[0] == 'b')
    if(buf[1] == 'u')
      if(buf[2] == 'g') {
        return 1/0;      // Defect: divide-by-zero.
      }
  return 0;
}

チュートリアルで使用されている testme アプリケーションはもうおなじみでしょう。

おさらいしておくと、Mayhem は testme ターゲットのファジングを試み、ゼロ除算の欠陥に到達するため、"bug" というテスト ケースを入力します。ここで、testme ターゲットに開始点となるテスト スイートが用意され、"a" から "z" のテスト ケースが含まれていたとします。明らかに、testme アプリケーションのさまざまな部分がカバーされるようになるのは "b" で始まるテスト ケースだけです。そのため、すべてのテスト ケース (一部は追加の実質的なコード カバレッジにつながらない) を使用して testme アプリケーションの実行を繰り返すのではなく、Mayhem ランの実行中に使用されるテスト ケースを文字 "b" で始まるものだけに限定するのが、ランを最適化するかしこい方法でしょう。

テスト ケースの最小化

ファザーはプログラムの実行を繰り返し、変化させたテスト ケースを入力として使用するため、テスト ケースはサイズの点でも複雑度の点でも膨張しがちです。これは、実行時間の延長につながるため、ユニークなコード カバレッジにつながるテスト ケースを発見するのがだんだん難しくなります。たとえば、4096 バイトのテスト ケースを 1 ビットずつ変化させようとすると、1 バイトのテストケースを 1 ビットずつ変化させるのと比べて長い時間がかかり、コード カバレッジの低下を招きます。

また、すでに大きなテスト ケースを変化させ、バイトを追加することに付加価値がない場合、テスト ケースを小さく保つべきです。たとえば、GET HTTP/1.1 を解析するプログラムは、常に 12 バイトしか解析しないため、4,000 バイトのテストケースを生成しても、12 バイトのテストケースと同じように扱われます。結果として、そのような大きなテストケースを生成する必要はなく、結局はターゲットのファジングの効率が低下します。

Info

このため、Mayhem が生成するテスト ケースのサイズを制限する max_length 機能が用意されています。これについては、「Mayhemfile Specification」を参照してください。

まとめ

ファザーの効率は、主に 2 つの要素に依存します。プログラムの実行スピードと、ファザーがコードに存在する新しいパスをどれだけすばやくカバーできるかという有効性です。

Mayhem でファズ ターゲットを最適化する方法を理解すると、時間を節約し、テスト対象のプログラムのコード カバレッジを増加させるのに役立ちます。