コンテンツにスキップ

ビギナー

Mayhem での非 Docker ターゲットのテスト

テスト対象は、Docker イメージに含まれていない通常のバイナリですか? このレッスンでは、Mayhem パッケージを使用して非 Docker ターゲットをパッケージ化し、ファジングする方法を説明します。


学習時間の目安: 30 分

このレッスンを終了すると、以下のことができるようになります。

  1. mayhem package コマンドを使用してターゲット バイナリをパッケージ化する。
  2. testme ターゲット バイナリに対して Mayhem ランを実行する。
  3. (Mayhemfile を修正せずに) ターゲットを置換してリグレッション テストをセットアップして実行する。
  4. パッケージ化されたターゲットに対してテスト ケースをシード化する。

レッスンを駆け足で

始める前に前提条件を確認してください。

  1. mayhem package コマンドを使用して testme バイナリをパッケージ化します。

    mayhem package testme -o /tmp/testme-pkg
    
  2. testme バイナリの Mayhemfile を構成します。

    1
    2
    3
    4
    5
    6
    project: testme
    target: testme
    duration: 90
    advanced_triage: true
    cmds:
      - cmd: /root/tutorial/testme/v1/testme @@
    
  3. mayhem run コマンドを使用して testme バイナリに対して Mayhem ランを実行します。

    mayhem run /tmp/testme-pkg
    

以下が必要です。

  • Mayhem CLI がインストールされ、Mayhem サーバーで認証されていること
  • Linux OS (macOS および Windows は mayhem package を完全にはサポートしません)

    そのため、Linux 環境として forallsecure/tutorial Docker イメージを使用して続けることを推奨します。

    docker pull forallsecure/tutorial:2.10
    docker run -ti --privileged --rm forallsecure/tutorial:2.10
    

ターミナル記録

mayhem package コマンドを使用する

Docker を使用できない場合、mayhem package コマンドを使用すると、ローカルなファイルシステムからアプリケーション バイナリのすべての依存関係を静的に推測し、アプリケーションおよび特定された依存関係を含むミニルート ファイルシステムを構築することで、自動的にアプリケーション バイナリをパッケージ化できます。

Note

ターゲット アプリケーションを適切に実行できるよう、検出されなかった依存関係はユーザーが手動でミニルート ファイルシステムに追加する必要があります。これが Docker を使用してターゲットをパッケージ化することが推奨される理由です。Docker では、ターゲット バイナリはすでに必要な環境と共にバンドルされており、潜在的な依存関係の問題を避けることができます。

mayhem package コマンドを使用するには、次のパラメーターを指定する必要があります。

mayhem package <path_to_binary> -o <output_path>

Mayhem で非 Docker ターゲットをパッケージ化してテストするための一般的なワークフローは次のとおりです。

  1. mayhem package コマンドを使用してターゲット バイナリをパッケージ化します。
  2. 生成されたランの Mayhemfile を構成します。
  3. mayhem run コマンドを使用して Mayhemfile の Mayhem ランを実行します。

Mayhem パッケージの概要がわかったところで、testme アプリケーションに対して mayhem package コマンドを実行する方法を確認しましょう。

testme バイナリをパッケージ化してテストする

これまでのレッスンでは、testme バイナリを Docker コンテナー内のコンテナー アプリケーションとしてパッケージ化してファジングしました。 しかし、Docker を使用できないが testme バイナリにアクセスする必要がある場合、Mayhem パッケージとして testme バイナリをパッケージ化してファジングします。

Important

mayhem package コマンドは Linux OS 環境でのみ動作します。そのため、macOS ユーザーは Linux OS 環境を利用するために forallsecure/tutorial Docker イメージを使用して以降の手順を行う必要があります。このレッスンは forallsecure/tutorial Docker イメージでの操作を前提としているため、もともと Linux を使用しているユーザーも同様にすることを推奨します。

docker pull forallsecure/tutorial:2.10
docker run -ti --privileged --rm forallsecure/tutorial:2.10

インターネット接続がなく、forallsecure/tutorial Docker イメージをプルできない場合、次をダウンロードして展開します: testme.zip

まず、forallsecure/tutorial:2.10 Docker イメージ内の /root/tutorial/testme/v1 に移動します。

1.testme バイナリをパッケージ化する

次に、testme アプリケーション バイナリをパッケージ化する必要があります。testme バイナリがあるディレクトリで次のコマンドを実行します。

mayhem package ./testme -o /tmp/testme-pkg

すると、/tmp/testme-pkg に次のパッケージおよびサブコンテンツが作成されます。

  1. Mayhemfile: Mayhem ランの構成ファイル
  2. testsuite: テスト ケース ファイルのリポジトリ
  3. root: アプリケーション バイナリおよび依存関係を含むミニルート ファイルシステム

その後、Mayhemfile を構成してから、パッケージ化されたターゲットに対して Mayhem ランを実行します。

2. Mayhemfile を構成する

mayhem package で生成された Mayhemfile は、Mayhem が mayhem package から推測したものを使用するよう自動で構成されています。ただし、Mayhem で適切にアプリケーションを実行するには、ユーザーが Mayhemfile をより詳細に構成する必要がある場合もあります。

Tip

より複雑なアプリケーション用に Mayhemfile を構成する際は、次の役に立つヒントを心にとめてください。

  • Mayhem は非特権ユーザーとしてターゲットを実行します。ターゲットが特権を必要とする場合、Mayhemfile の最上位の構成オプションとして uid を設定する必要があります。たとえば、root としてアプリケーションを実行する必要がある場合、Mayhemfileuid: 0 を設定します。

  • デフォルトでは、Mayhem パッケージは Debian "Buster" Docker イメージ内で実行されます。つまり、パッケージは Debian が提供する libc およびその他のライブラリを使用します。最上位のディレクティブ image を使用してベース OS イメージを変更できます。たとえば、アプリケーションが Alpine Linux で実行される場合、image: alpine を使用します。

  • Mayhem は非常に限定されたユーザー環境でパッケージを実行します。特定の cmd の環境変数を設定する場合、リストとして env を設定します。たとえば、アプリケーションの PATH に /usr/local/bin を追加するには、env: { "PATH": "/usr/local/bin:/usr/bin"} を設定します。

mayhem package によって生成される Mayhemfile は次のようになります。

 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
# Mayhem by https://forallsecure.com
# Mayhemfile: configuration file for testing your target with Mayhem
# Format: YAML 1.1

# Owned project name that the target belongs to
project: testme

# Target name (should be unique within the project)
target: testme

# Time to spend testing.
duration: 90

# Image to run the binary in.
# image: $MAYHEM_DOCKER_REGISTRY/forallsecure/debian-buster:latest

# Turns on extra test case processing (completing a run will take longer)
advanced_triage: false

# Configure tasks to be run. If null or missing, defaults to exploitability factors, regression testing, and behavior testing.
tasks:
  - name: exploitability_factors
  - name: regression_testing
  - name: behavior_testing

# List of commands used to test the target
cmds:

  # Command used to start the target, "@@" is the input file
  # (when "@@" is omitted Mayhem defaults to stdin inputs)
  - cmd: /root/tutorial/testme/v1/testme @@
    env: {}

このレッスンでは、Mayhem ランの duration90 を設定します (デフォルトでは、Mayhem ランの継続期間は無期限に設定されます)。上記の例では、testme バイナリの Mayhem ランは約 90 秒間実行され、バイナリの場所として /root/tutorial/testme/v1/testme を指していることがわかります。このフォルダーは、mayhem package が作成する 3 つのアセット (Mayhefiletestsuite、および root) のroot フォルダーに相当します。この Mayhem ランは、@@ が示すとおり、ファイル入力メソッドを使用して testme バイナリをテストします。

Info

forallsecure/tutorial Docker イメージを使用していない場合、自動生成された cmd ファイルパスは、上記の例と異なる可能性があります。cmd ファイルパスはターゲット アプリケーションがもともとパッケージ化された場所に基づいて自動生成されます。

Mayhemfile を適切に構成したら、構成済みの Mayhemfile を含む mayhem package ディレクトリを指定して mayhem run コマンドを実行できます。

3. Mayhem ランを実行する

最後に、構成済みの Mayhemfile に対して mayhem run コマンドを実行します。

mayhem run /tmp/testme-pkg

Info

Mayhem ランを正常に実行するには、あらかじめ mayhem login コマンドを使用して Mayhem サーバーで認証を行う必要があります。詳細については「Mayhem サーバーでの認証」を参照してください。

これで終わりです。90 秒以内に testme バイナリで不適切な入力検証の欠陥が発見されるはずです。Mayhem UI で結果を参照することができます。

first-run.png

ターゲットを置換してリグレッション テストを実行する

これまでは、リグレッション テストを実行する際、Mayhemfile の project および target 構成値は変えず、cmd パラメーターを使用して修正/アップデートされた testme バイナリを指定して Mayhem ランを実行していました。

Note

こうすることで、同じ projecttarget の以前に生成されたテスト ケースを新しいランで効果的に利用し、以前の結果 (クラッシュを起こす) テスト ケースと新しい 変更された testme バイナリの変更された (修正された) 動作を比較しました。

しかし、Mayhemfile で指定されるターゲット バイナリを更新/修正されたバージョンのアプリケーションに直接置き換えることで、Mayhemfile の構成を変更せずにリグレッション テストを実行することもできます。

更新されたバイナリ アプリケーションのリグレッション テストをセットアップする方法は 2 つあります。

  1. project および target Mayhemfile パラメーターを再利用し、 cmd パラメーターが更新/修正されたバイナリの場所を指すよう設定します (説明はこちら) 。
  2. 以前のバイナリを更新されたバージョンに置き換え、同じ Mayhemfile 構成を維持します。

/tmp/testme-pkg Mayhem パッケージ内の testme バイナリに直接アクセスすることができるので、直接ターゲットを置換してリグレッション テストをセットアップする方法を確認してみましょう。

Info

Docker ターゲットも非 Docker ターゲットも、両方のリグレッション テスト セットアップ方法を利用できます。ただし、Docker ターゲットの場合、ターゲット バイナリを直接置換するのはより手間がかかります。Docker イメージをインタラクティブ モードで実行し、ターゲット バイナリを置き換え、変更を Docker イメージにコミットし、更新された Docker イメージを Docker Hub または Mayhem Docker レジストリに再度プッシュする必要があります。そのため、リグレッション テスト用にターゲットを置き換える 2 番目の方法は、この非 Docker ターゲットのレッスンで説明されています。

/root/tutorial/testme/v2 ディレクトリには、すでに以前の testme バイナリを修正されたバージョンに置き換えるための run.sh スクリプトがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/sh

# Copy over the new, fixed version into the package.
#
# Note that we are *overwriting* the old testme application.
# Overwriting an old version allows you to use your existing
# Mayhemfile
cp testme /tmp/testme-pkg/root/root/tutorial/testme/v1/testme

# Re-run mayhem. There is no need to edit the Mayhemfile.
# The run ID is saved to $id
id=$(mayhem run --regression /tmp/testme-pkg)

# Wait for the run to finish
mayhem wait $id --regression

# Sync the test suite to the "testsuite" directory.
mayhem sync /tmp/testme-pkg

Info

mayhem wait コマンドに --regression パラメーターを指定すると、リグレッション テスト フェーズが完了するまで待機します。

/tmp/testme-pkg/root/root/tutorial/testme/v1/testme の以前にパッケージ化された testme バイナリが更新された testme バイナリに置き換えられ、パッケージ化された Mayhemfile に対してリグレッション テストが実行されることがわかります。

/root/tutorial/testme/v2 ディレクトリで次のスクリプトを実行します。

sh run.sh

Mayhem UI で該当ランを参照すると、リグレッション テストの結果で、以前はクラッシュしていたテスト ケースおよび関連する欠陥が修正済みとマークされていることがわかります。

regression-testing-ui.png

これで終わりです。これで、リグレッション テストをセットアップする 2 種類のワークフローのやり方を学びました。

シード化されたテスト スイートを使用して testme をテストする

Mayhem ランを実行する前に、testsuite ディレクトリ内の既存のテスト ケースをスキャンして、Mayhem ランのスピードとカバレッジを向上させるためのシードまたは「ジャンプスタート」として利用できます。testsuite ディレクトリ内の各ファイルは個々の入力テスト ケースおよびその内容を表しています。

Info

指定された既存のテスト ケースがあるテスト スイート フォルダーを Mayhem ランのシードとして使用するよう Mayhemfile を構成する方法の詳細については testsuite パラメーターを参照してください。シード化されたテスト スイートは、Docker ターゲットにも非 Docker ターゲットにも設定できます。

例として、ターゲットの基になる testme のコードをもう一度見てみましょう。/tmp/testme-pkg にある Mayhem パッケージに対して、次の内容を含む seed.txt ファイルを作成して testsuite フォルダーに置いたとします。

bu

すると、指定された bu テスト ケースは (aa などのテストケースと比べて) 最初からより深く testme バイナリをカバーできるため、シード化されたテスト ケースは Mayhem ランのパフォーマンスを効果的に最適化できるでしょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int fuzzme(char *buf)
{
  if(buf[0] == 'b')
    if(buf[1] == 'u')
      if(buf[2] == 'g') {
        printf("You've got it!");
        abort(); // Defect: SIGABRT.
      }
  return 0;
}

そのため、(内容として bu を含む) seed.txt/tmp/testme-pkg フォルダーの testsuite ディレクトリに置きます。

cp seed.txt /tmp/testme-pkg/testsuite

次に、Mayhemfile の target パラメーターの値を testme-seed に変更します。こうすると、手動でシード化されたテスト スイートを使用するフレッシュな Mayhem ランが開始されます (デフォルトでは、Mayhem ランは同じ <project>/<target> の前回のランで生成されたテスト スイートを使用して現行ランをシード化します)。

1
2
3
4
5
6
project: testme
target: testme-seed
duration: 90
advanced_triage: true
cmds:
  - cmd: /root/tutorial/testme/v1/testme @@

ここで mayhem run コマンドを再実行し、testme に対してシード化されたテスト スイートを使用します。

$ mayhem run /tmp/testme-pkg
/tmp/tmpo2ifgn0f/testsuite.tgz 100% |#############| Time:  0:00:00 224.7 B/s
Syncing /tmp/testme-pkg/testsuite 100% |####################| Time:  0:00:01
Run started: testme/testme-seed/1
testme/testme-seed/1

testme バイナリの Mayhem ランは、前回のシード化されていないランに比べて、より速く潜在的な欠陥を発見できたはずです。これは、シード テスト ケース bu が、不適切な入力検証の欠陥に至る入力 "bug" をチェックする if 文をカバーするのに適したスタート地点を提供したため、Mayhem が 行わなければならないプログラムの探索が少なくなったからです。

現実的な演習: testme-npd バイナリをシード化してテストする

バイナリをパッケージ化する方法およびテスト スイートをシード化する方法がわかったので、前のレッスンで使用した testme-npd に適応できるかやってみましょう。

Note

forallsecure/tutorial Docker イメージを使用している場合、testme-npd.zip を Docker コンテナーに移動するには、docker cp コマンドを使用する必要がある場合があります。詳細については docker cp に関する公式ドキュメントを参照してください。

手順

  1. 次をダウンロードして展開します: testme-npd

  2. mayhem package コマンドを使用して testme-npdバイナリをパッケージ化します。

  3. Mayhemfileproject および target に値 testme-npd を設定します。duration60 秒を設定します。

  4. testme-npd パッケージの testsuite ディレクトリに seed.txt テスト ケース ファイルを用意します。

  5. mayhem run コマンドを使用して Mayhem ランを実行します。

Tip

この Mayhem ランは 60 秒しか実行されません。null ポインター間接参照のバグの発見をスピード アップしたいので、どのようなテキストが null ポインター参照のバグをより効率的にカバーできるかを考えてみましょう。

🔍 確認testme-npd バイナリをシード化してテストする

解答

まず、mayhem package コマンドを使用して testme-npd バイナリをパッケージ化する必要があります。

mayhem package testme-npd -o /tmp/testme-npd-pkg

Mayhemfileprojecttarget、および duration パラメーターに適切な値を指定する必要があります。

 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
# Mayhem by https://forallsecure.com
# Mayhemfile: configuration file for testing your target with Mayhem
# Format: YAML 1.1

# Owned project name that the target belongs to
project: testme-npd

# Target name (should be unique within the project)
target: testme-npd

# Base image to run the binary in.
# image: $MAYHEM_DOCKER_REGISTRY/forallsecure/debian-buster:latest

# Time that analysis will run for in seconds - if absent, run forever
duration: 90

# Turns on extra test case processing (completing a run will take longer)
advanced_triage: false

# List of commands used to test the target
cmds:

  # Command used to start the target, "@@" is the input file
  # (when "@@" is omitted Mayhem defaults to stdin inputs)
  - cmd: /root/testme-npd/testme @@
    env: {}

    ...

次に、"nul" を内容とする seed.txt を作成し、testsuite ディレクトリに置く必要があります。seed.txt テスト ケース ファイルが null ポインター間接参照につながる if 文をカバーするのに適したスタート地点を提供するため、null ポインター間接参照のバグがより早く発見されるはずです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int testme(char *buf, unsigned len)
{
  if(buf[0] == 'b')
    if(buf[1] == 'u')
      if(buf[2] == 'g') {
        abort(); // Defect: SIGABRT.  
      }

  if(buf[0] == 'n')
    if(buf[1] == 'u')
      if(buf[2] == 'l')
        if(buf[3] == 'l') {
          int *p = NULL; 
          int y = *p; // Defect: null pointer dereference
        }

  return 0;
}

最後に、mayhem run コマンドを実行すると、次のようなラン ページが表示されます。

mayhem run /tmp/testme-npd-pkg

testme-npd-run-page

よくできました! 自力でファズ ターゲットをビルドし、最適化することができました。

✏️ まとめと振り返り

このレッスンでは、mayhem package コマンドを使用してアプリケーションをパッケージ化し、生成された Mayhemfile を構成し、Mayhem ランを実行し、さらにはテスト スイートをシード化して解析とテストのパフォーマンスを向上させる方法を学びました。


学習内容

1. mayhem package コマンドを使用してターゲット バイナリをパッケージ化する
  • mayhem package コマンドを使用すると、アプリケーションのすべての依存関係を静的に推測し、アプリケーションおよび特定された依存関係を含むミニルート ファイルシステムを構築することで、自動的にアプリケーション バイナリをパッケージ化できます。

  • mayhem package コマンドの書式は次のとおりです。

    mayhem package <path_to_binary> -o <output_path>
    

  • パッケージの内容は次のようになります。The package contents should look similar to the following:

    ├── /tmp/testme-pkg
        └── Mayhemfile - Mayhem ランの構成ファイル
        └── testsuite - テスト ケース ファイルのリポジトリ
        └── root - アプリケーション バイナリを含むミニルート ファイルシステム
    

2. パッケージ化されたターゲットに対して Mayhem ランを実行する
  • ターゲットをパッケージ化したら、ターゲット パッケージ内に作成された Mayhemfile に対して mayhem run コマンドを使用できます。
3. ターゲットを置換して (Mayhemfile の変更なしに) リグレッション テストをセット アップし、実行する
  • アップデートされたバイナリ アプリケーションのリグレッション テストをセットアップする方法は 2 つあります。

    1. project および target Mayhemfile パラメーターを再利用し、cmd パラメーターがアップデート/修正されたバイナリを指すよう設定します (説明はこちら)。
    2. 修正されたバイナリで以前のバイナリを置き換え、Mayhemfile 構成をそのまま利用します。
  • directory /root/tutorial/testme/v2 ディレクトリには、以前の testme バイナリを修正されたバージョンに自動的に置き換える run.sh スクリプトがすでに用意されています。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #!/bin/sh
    
    # Copy over the new, fixed version into the package.
    #
    # Note that we are *overwriting* the old testme application.
    # Overwriting an old version allows you to use your existing
    # Mayhemfile
    cp testme /tmp/testme-pkg/root/root/tutorial/testme/v1/testme
    
    # Re-run mayhem. There is no need to edit the Mayhemfile.
    # The run ID is saved to $id
    id=$(mayhem run --regression /tmp/testme-pkg)
    
    # Wait for the run to finish
    mayhem wait $id --regression
    
    # Sync the test suite to the "testsuite" directory.
    mayhem sync /tmp/testme-pkg
    
4. パッケージ化されたターゲットのテスト ケース ファイルをシード化する
  • Mayhem ランを実行する前に、testsuite ディレクトリ内の既存のテスト ケースをスキャンして、Mayhem ランのスピードとカバレッジを向上させるためのシードまたは「ジャンプスタート」として利用できます。testsuite ディレクトリ内の各ファイルは個々の入力テスト ケースおよびその内容を表しています。

  • たとえば、/tmp/testme-pkg にある Mayhem パッケージの元になっている testme のコードでは、次の内容を含む seed.txt ファイルを作成して testsuite フォルダーに置きます。

    bu
    
  • すると、bu テスト ケースは (aa などのテストケースと比べて) 最初からより深く testme バイナリをカバーできるため、シード化されたテスト ケースは Mayhem ランのパフォーマンスを効果的に最適化できるでしょう。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    int fuzzme(char *buf)
    {
      if(buf[0] == 'b')
        if(buf[1] == 'u')
          if(buf[2] == 'g') {
            printf("You've got it!");
            abort(); // Defect: SIGABRT.
          }
      return 0;
    }