mcpField
H
harness-writing

harness-writing

github.com/trailofbitsテスト・品質7.9k5.5k 回インストール今日更新
リポジトリ

harness-writing について

harness-writingは、ファザー入力をアプリケーションコードにつなぐハーネスを作る開発者向けのファジングスキルです。入力処理、構造化データ、決定性に加え、C/C++とRustの実装パターンを扱い、対象APIを再現性のある形でテストします。

主な機能

  1. 01適切なファジング入口の選定
  2. 02入力サイズの処理と検証
  3. 03バイト列からの構造化データ抽出
  4. 04単一ハーネスで関連操作をテスト
  5. 05状態のリセットと実行の決定性維持
  6. 06ファジング中のリソース管理

活用シーン

  1. 01パーサーやバリデーター用ハーネスの作成
  2. 02構造化入力が必要なAPIのファジング
  3. 03キャンペーン停滞時のカバレッジ改善
  4. 04実行をまたいだクラッシュ再現性の確保

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/trailofbits/skills --skill harness-writing

インストール後、Claude Code で/harness-writingを入力して読み込まれたか確認します。

同梱ファイルagents/openai.yamlassets/trail-of-bits-mark.svg

詳細

ファジングハーネスの作成

ファジングハーネスは、ファザーからランダムなデータを受け取り、テスト対象システム(SUT)に渡すエントリーポイント関数です。ハーネスの品質によって、どのコードパスが実行されるか、また重大なバグが見つかるかどうかが直接決まります。ハーネスの作りが不適切だと、サブシステム全体がテストされなかったり、再現不可能なクラッシュが発生したりすることがあります。

概要

ハーネスは、ファザーによるランダムなバイト生成とアプリケーションの API をつなぐ役割を担います。生のバイト列を意味のある入力に解析し、対象関数を呼び出し、エッジケースを適切に処理する必要があります。ファジング環境で最も重要なのはハーネスです。作りが不適切だと、アプリケーションの重要な部分がテストされない可能性があります。

重要な概念
概念説明
ハーネスファザーの入力を受け取り、テスト対象のコードを呼び出す関数
SUTテスト対象システム(System Under Test)。ファジングの対象となるコード
エントリーポイントファザーが必要とする関数シグネチャ(例:LLVMFuzzerTestOneInput)
FuzzedDataProvider生のバイト列から型付きデータを構造的に取り出すためのヘルパークラス
決定性同じ入力が常に同じ動作を引き起こすことを保証する性質
インターリーブファジング入力に応じて複数の操作を実行する単一のハーネス

適用する場面

この手法を適用する場合:

  • 初めて新しいファズターゲットを作成するとき
  • ファジングキャンペーンのコードカバレッジが低い、またはバグが見つからないとき
  • ファジング中に見つかったクラッシュを再現できないとき
  • 対象の API が複雑な入力や構造化された入力を必要とするとき
  • 関連する複数の関数をまとめてテストするとき

この手法を適用しない場合:

  • プロジェクトに既存の十分にテストされたハーネスがあるとき
  • ツールがニーズに合ったハーネスを自動生成するとき
  • 対象に包括的なファジング基盤がすでに整っているとき

クイックリファレンス

タスクパターン
最小限の C++ ハーネスextern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
最小限の Rust ハーネス`fuzz_target!(
サイズの検証if (size < MIN_SIZE) return 0;
整数へのキャストuint32_t val = *(uint32_t*)(data);
FuzzedDataProvider の使用FuzzedDataProvider fuzzed_data(data, size);
型付きデータの取り出し(C++)auto val = fuzzed_data.ConsumeIntegral<uint32_t>();
文字列の取り出し(C++)auto str = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);

手順

ステップ 1:エントリーポイントを特定する

コードベースから、次のような関数を探します。

  • 外部入力を受け取る(パーサー、バリデーター、プロトコルハンドラー)
  • 複雑なデータ形式を解析する(JSON、XML、バイナリプロトコル)
  • セキュリティ上重要な処理を実行する(認証、暗号処理)
  • サイクロマティック複雑度が高い、または分岐が多い

一般的なターゲットは次のとおりです。

  • プロトコルパーサー
  • ファイル形式パーサー
  • シリアライズ/デシリアライズ関数
  • 入力検証ルーチン
ステップ 2:最小限のハーネスを作成する

まず、対象の関数を呼び出す、できる限りシンプルなハーネスを作成します。

C/C++:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    target_function(data, size);
    return 0;
}

Rust:

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    target_function(data);
});
ステップ 3:入力検証を追加する

意味のある入力として小さすぎる、または大きすぎる入力を拒否します。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Ensure minimum size for meaningful input
    if (size < MIN_INPUT_SIZE || size > MAX_INPUT_SIZE) {
        return 0;
    }
    target_function(data, size);
    return 0;
}

理由: ファザーはあらゆるサイズのランダムな入力を生成します。ハーネス自体で予期しない問題が起きないよう、空の入力、極端に小さい入力、巨大な入力、不正な入力を処理する必要があります(SUT でのクラッシュは問題ありません。それを見つけることが目的です)。

ステップ 4:入力を構造化する

整数や文字列などの型付きデータを必要とする API では、キャストや FuzzedDataProvider などのヘルパーを使用します。

単純なキャスト:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    if (size != 2 * sizeof(uint32_t)) {
        return 0;
    }

    uint32_t numerator = *(uint32_t*)(data);
    uint32_t denominator = *(uint32_t*)(data + sizeof(uint32_t));

    divide(numerator, denominator);
    return 0;
}

FuzzedDataProvider を使用:

#include "FuzzedDataProvider.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    FuzzedDataProvider fuzzed_data(data, size);

    size_t allocation_size = fuzzed_data.ConsumeIntegral<size_t>();
    std::vector<char> str1 = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);
    std::vector<char> str2 = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);

    concat(&str1[0], str1.size(), &str2[0], str2.size(), allocation_size);
    return 0;
}
ステップ 5:テストして反復する

ファザーを実行し、次の点を確認します。

  • コードカバレッジ(興味深いコードパスに到達しているか)
  • 1 秒あたりの実行回数(十分な速度が出ているか)
  • クラッシュの再現性(保存した入力でクラッシュを再現できるか)

これらの指標が改善するように、ハーネスを調整します。

よくあるパターン

パターン:バイト配列の先へ—整数へのキャスト

ユースケース: 対象が整数や浮動小数点数などのプリミティブ型を想定している場合

実装:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Ensure exactly 2 4-byte numbers
    if (size != 2 * sizeof(uint32_t)) {
        return 0;
    }

    // Split input into two integers
    uint32_t numerator = *(uint32_t*)(data);
    uint32_t denominator = *(uint32_t*)(data + sizeof(uint32_t));

    divide(numerator, denominator);
    return 0;
}

Rust での同等のコード:

fuzz_target!(|data: &[u8]| {
    if data.len() != 2 * std::mem::size_of::<i32>() {
        return;
    }

    let numerator = i32::from_ne_bytes([data[0], data[1], data[2], data[3]]);
    let denominator = i32::from_ne_bytes([data[4], data[5], data[6], data[7]]);

    divide(numerator, denominator);
});

有効な理由: 8 バイトの入力はすべて有効です。ファザーは入力が正確に 8 バイトである必要があることを学習し、ビット反転のたびに新しい、興味深い可能性のある入力が生成されます。

パターン:複雑な入力に FuzzedDataProvider を使用する

ユースケース: 対象が複数の文字列、整数、または可変長データを必要とする場合

実装:

#include "FuzzedDataProvider.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    FuzzedDataProvider fuzzed_data(data, size);

    // Extract different types of data
    size_t allocation_size = fuzzed_data.ConsumeIntegral<size_t>();

    // Consume variable-length strings with terminator
    std::vector<char> str1 = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);
    std::vector<char> str2 = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);

    char* result = concat(&str1[0], str1.size(), &str2[0], str2.size(), allocation_size);
    if (result != NULL) {
        free(result);
    }

    return 0;
}

役立つ理由: FuzzedDataProvider は、バイトストリームから構造化データを抽出する複雑さに対処します。異なる型の複数のパラメーターを必要とする API で特に役立ちます。

パターン:インターリーブファジング

ユースケース: 関連する複数の操作を 1 つのハーネスでテストする場合

実装:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    if (size < 1 + 2 * sizeof(int32_t)) {
        return 0;
    }

    // First byte selects operation
    uint8_t mode = data[0];

    // Next bytes are operands
    int32_t numbers[2];
    memcpy(numbers, data + 1, 2 * sizeof(int32_t));

    int32_t result = 0;
    switch (mode % 4) {
        case 0:
            result = add(numbers[0], numbers[1]);
            break;
        case 1:
            result = subtract(numbers[0], numbers[1]);
            break;
        case 2:
            result = multiply(numbers[0], numbers[1]);
            break;
        case 3:
            result = divide(numbers[0], numbers[1]);
            break;
    }

    // Prevent compiler from optimizing away the calls
    printf("%d", result);
    return 0;
}

利点:

  • 個別のハーネスを複数作成するより、1 つのハーネスを作成するほうが速い
  • コーパスを共有するため、ある操作で興味深い入力が、ほかの操作でも興味深い入力になる場合がある
  • 操作間の相互作用に起因するバグを発見できる

使用する場面:

  • 操作が類似した入力型を共有している
  • 操作に論理的な関連がある(例:算術演算、CRUD 操作)
  • すべての操作で単一のコーパスを使うのが適切
パターン:Arbitrary を使った構造認識型ファジング(Rust)

ユースケース: カスタム構造体を使用する Rust コードをファジングする場合

実装:

use arbitrary::Arbitrary;

#[derive(Debug, Arbitrary)]
pub struct Name {
    data: String
}

impl Name {
    pub fn check_buf(&self) {
        let data = self.data.as_bytes();
        if data.len() > 0 && data[0] == b'a' {
            if data.len() > 1 && data[1] == b'b' {
                if data.len() > 2 && data[2] == b'c' {
                    process::abort();
                }
            }
        }
    }
}

arbitrary を使ったハーネス:

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: your_project::Name| {
    data.check_buf();
});

Cargo.toml に追加:

[dependencies]
arbitrary = { version = "1", features = ["derive"] }

役立つ理由: arbitrary クレートは、生のバイトから Rust の構造体へのデシリアライズを自動的に処理するため、定型コードを減らし、有効な構造体を確実に構築できます。

制限事項: arbitrary クレートは逆方向のシリアライズに対応していないため、特定の構造体に対応するバイト配列を手動で構築できません。この方法は空のコーパスから開始する場合に最適です(libFuzzer では問題ありませんが、AFL++ では問題になります)。

高度な使用方法

ヒントとコツ
ヒント役立つ理由
まずパーサーから始めるバグ密度が高く、明確なエントリーポイントがあり、ハーネスを簡単に作成できます
I/O 操作をモックするブロッキング I/O によるハングを防ぎ、決定性を確保できます
FuzzedDataProvider を使用する生のバイトから構造化データを簡単に抽出できます
グローバル状態をリセットする各反復が独立し、再現可能になります
ハーネス内でリソースを解放する長時間のキャンペーン中のメモリ枯渇を防ぎます
ハーネス内でのログ出力を避けるログ出力は遅いため、ファジングには毎秒数百~数千回の実行が必要です
まずハーネスを手動でテストするキャンペーンを開始する前に、既知の入力でハーネスを実行します
早い段階でカバレッジを確認するハーネスが想定されるコードパスに到達することを確認します
Protocol Buffers による構造認識型ファジング

高度に構造化された入力形式の場合は、カスタムミューテーターと組み合わせた中間形式として Protocol Buffers の使用を検討してください。

// Define your input format in .proto file
// Use libprotobuf-mutator to generate valid mutations
// This ensures fuzzer mutates message contents, not the protobuf encoding itself

この方法はセットアップの手間が増えますが、パースできない入力にファザーが時間を費やすのを防ぎます。詳細は構造認識型ファジングのドキュメントを参照してください。

非決定性への対処

問題: ランダム値やタイミングへの依存により、クラッシュを再現できません。

解決策:

  • rand() を、ファザー入力からシードを取得する決定論的な PRNG に置き換えます。
    uint32_t seed = fuzzed_data.ConsumeIntegral<uint32_t>();
    srand(seed);
    
  • 時刻、PID、またはランダムデータを返すシステムコールをモックします
  • /dev/random や /dev/urandom からの読み取りを避けます
グローバル状態のリセット

SUT がグローバル状態(シングルトン、静的変数)を使用する場合は、反復の間にリセットします。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Reset global state before each iteration
    global_reset();

    target_function(data, size);

    // Clean up resources
    global_cleanup();
    return 0;
}

理由: グローバル状態があると、特定の入力ではなく N 回目の反復後にクラッシュすることがあり、バグを再現できなくなる場合があります。

実用的なハーネスのルール

効果的なファジングハーネスにするため、次のルールに従ってください。

ルール理由
あらゆる入力サイズに対応するファザーは空の入力、小さな入力、大きな入力を生成するため、ハーネスは適切に処理する必要があります
exit()を呼び出さないexit()を呼び出すとファザープロセスが停止します。必要な場合はSUT内でabort()を使用してください
すべてのスレッドを終了まで待つ次のイテレーションを開始する前に、各イテレーションを完了させる必要があります
高速にする1秒あたり数百〜数千回の実行を目指してください。ログ出力、高い計算量、過剰なメモリ使用は避けてください
決定性を維持する再現性を保つため、同じ入力には常に同じ動作を返す必要があります
グローバル状態を避けるグローバル状態は再現性を損なうため、避けられない場合はイテレーション間でリセットしてください
対象を絞る同じハーネスでPNGとTCPをファジングしないでください。形式ごとに別のターゲットが必要です
リソースを解放する長時間の実行中にリソースが枯渇しないよう、メモリリークを防いでください

注記: これらのガイドラインはハーネスコードだけでなく、SUT全体にも適用されます。SUTがこれらのルールに違反する場合は、パッチを適用することを検討してください(ファジングの障害に関するテクニックを参照)。

アンチパターン

アンチパターン問題適切な方法
リセットされないグローバル状態クラッシュが非決定的になりますハーネスの開始時にすべてのグローバル状態をリセットします
ブロッキングI/Oやネットワーク呼び出しファザーがハングし、時間を浪費しますI/Oをモックし、メモリ内バッファーを使用します
ハーネス内のメモリリークリソース枯渇によりキャンペーンが停止します戻る前にすべての割り当てを解放します
SUT内でのexit()の呼び出しファジングプロセス全体が停止しますabort()またはエラーコードを返します
ハーネスでの大量のログ出力1秒あたりの実行回数が桁違いに減少しますファジング中はログ出力を無効にします
1イテレーションあたりの操作が多すぎるファザーの速度が低下しますイテレーションを高速かつ集中的に保ちます
無関係な入力形式を混在させるコーパスのエントリーを形式間で活用できません形式ごとにハーネスを分けます
入力サイズを検証しないエッジケースでハーネスがクラッシュしますdataにアクセスする前にsizeを確認します

ツールごとのガイダンス

libFuzzer

ハーネスのシグネチャ:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Your code here
    return 0;  // Non-zero return is reserved for future use
}

コンパイル:

clang++ -fsanitize=fuzzer,address -g harness.cc -o fuzz_target

統合のヒント:

  • 構造化入力の抽出にはFuzzedDataProvider.hを使用します
  • ファジングランタイムをリンクするには、-fsanitize=fuzzerを指定してコンパイルします
  • より多くのバグを検出するには、サニタイザー(-fsanitize=address,undefined)を追加します
  • クラッシュ発生時のスタックトレースを改善するには、-gを使用します
  • libFuzzerは空のコーパスから開始できるため、シード入力は不要です

実行:

./fuzz_target corpus_dir/

リソース:

AFL++

AFL++は複数のハーネス形式に対応しています。最高のパフォーマンスを得るには、永続モードを使用してください。

永続モードのハーネス:

#include <unistd.h>

int main(int argc, char **argv) {
    #ifdef __AFL_HAVE_MANUAL_CONTROL
        __AFL_INIT();
    #endif

    unsigned char buf[MAX_SIZE];

    while (__AFL_LOOP(10000)) {
        // Read input from stdin
        ssize_t len = read(0, buf, sizeof(buf));
        if (len <= 0) break;

        // Call target function
        target_function(buf, len);
    }

    return 0;
}

コンパイル:

afl-clang-fast++ -g harness.cc -o fuzz_target

統合のヒント:

  • 10〜100倍の高速化には、永続モード(__AFL_LOOP)を使用します
  • セットアップのオーバーヘッドを省くには、初期化の遅延(__AFL_INIT())を検討します
  • AFL++では、コーパスディレクトリに少なくとも1つのシード入力が必要です
  • サニタイザーを使ったビルドには、AFL_USE_ASAN=1またはAFL_USE_UBSAN=1を使用します

実行:

afl-fuzz -i seeds/ -o findings/ -- ./fuzz_target
cargo-fuzz(Rust)

ハーネスのシグネチャ:

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // Your code here
});

構造化入力を使用する場合(arbitraryクレート):

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: YourStruct| {
    data.check();
});

ハーネスの作成:

cargo fuzz init
cargo fuzz add my_target

統合のヒント:

  • 構造体を自動的にデシリアライズするには、arbitraryクレートを使用します
  • cargo-fuzzはlibFuzzerをラップしているため、libFuzzerのすべての機能を使用できます
  • サニタイザーを使ったコンパイルはcargo-fuzzによって自動的に行われます
  • ハーネスはfuzz/fuzz_targets/ディレクトリに配置します

実行:

cargo +nightly fuzz run my_target

リソース:

go-fuzz

ハーネスのシグネチャ:

// +build gofuzz

package mypackage

func Fuzz(data []byte) int {
    // Call target function
    target(data)

    // Return codes:
    // -1 if input is invalid
    //  0 if input is valid but not interesting
    //  1 if input is interesting (e.g., added new coverage)
    return 0
}

ビルド:

go-fuzz-build

統合のヒント:

  • カバレッジが増える入力には 1 を返します(任意。ファザーが自動的に検出できます)
  • 無効な入力には -1 を返し、類似した変異の優先度を下げます
  • go-fuzz が永続化を自動的に処理します

実行:

go-fuzz -bin=./mypackage-fuzz.zip -workdir=fuzz

トラブルシューティング

問題原因解決策
実行回数/秒が少ないハーネスが遅すぎます(ログ記録、I/O、複雑さ)ハーネスをプロファイリングし、ボトルネックを取り除き、I/O をモックします
クラッシュが見つからないカバレッジがバグのあるコードに到達していませんカバレッジを確認し、より多くのパスに到達するようハーネスを改善します
再現できないクラッシュ非決定性またはグローバル状態ランダム性を取り除き、反復ごとにグローバル変数をリセットします
ファザーがすぐに終了するハーネスが exit() を呼び出していますexit() を abort() またはエラーの返却に置き換えます
メモリ不足エラーハーネスまたは SUT のメモリリーク割り当てを解放し、リークサニタイザーを使ってリークを検出します
空の入力でクラッシュするハーネスがサイズを検証していませんif (size < MIN_SIZE) return 0; を追加します
コーパスが増えない入力の制約が厳しすぎる、または形式が厳格すぎるFuzzedDataProvider または構造を考慮したファジングを使います

関連スキル

この手法を使うツール
スキル適用方法
libfuzzerFuzzedDataProvider とともに LLVMFuzzerTestOneInput ハーネスシグネチャを使用します
aflppパフォーマンス向上のため、__AFL_LOOP を使う永続モードのハーネスをサポートします
cargo-fuzzRust 固有の fuzz_target! マクロと arbitrary クレートの統合を使用します
atherisPython ハーネスがバイト列を受け取り、Python 関数を呼び出します
ossfuzzクラウドファジング用に、特定のディレクトリ構造に配置されたハーネスが必要です
関連する手法
スキル関係
coverage-analysisハーネスの有効性、つまり対象コードに到達しているかを測定します
address-sanitizerハーネスが発見したバグ(バッファーオーバーフロー、解放後使用)を検出します
fuzzing-dictionaryハーネスで形式チェックを通過できるよう、ファザーにトークンを提供します
fuzzing-obstaclesSUT がハーネスのルール(終了、非決定性)に違反した場合にパッチを適用します

リソース

主な外部リソース

libFuzzer での入力分割 - Google Fuzzing Docs マジックセパレーターや FuzzedDataProvider の使用など、単一のファジングハーネスで複数の入力パラメーターを処理する手法を説明しています。

Protocol Buffers を使った構造を考慮したファジング カスタムミューテーターとともに protobuf を中間形式として使用し、形式のエンコーディングではなくメッセージの内容が変異するようにする高度な手法です。

libFuzzer のドキュメント ハーネスの要件、ベストプラクティス、高度な機能を網羅した LLVM の公式ドキュメントです。

cargo-fuzz の書籍 cargo-fuzz と arbitrary クレートを使った Rust のファジングハーネスの作成方法を解説する包括的なガイドです。

動画リソース

コメント