harness-writing について
harness-writingは、ファザー入力をアプリケーションコードにつなぐハーネスを作る開発者向けのファジングスキルです。入力処理、構造化データ、決定性に加え、C/C++とRustの実装パターンを扱い、対象APIを再現性のある形でテストします。
主な機能
- 01適切なファジング入口の選定
- 02入力サイズの処理と検証
- 03バイト列からの構造化データ抽出
- 04単一ハーネスで関連操作をテスト
- 05状態のリセットと実行の決定性維持
- 06ファジング中のリソース管理
活用シーン
- 01パーサーやバリデーター用ハーネスの作成
- 02構造化入力が必要なAPIのファジング
- 03キャンペーン停滞時のカバレッジ改善
- 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 または構造を考慮したファジングを使います |
関連スキル
この手法を使うツール
| スキル | 適用方法 |
|---|---|
| libfuzzer | FuzzedDataProvider とともに LLVMFuzzerTestOneInput ハーネスシグネチャを使用します |
| aflpp | パフォーマンス向上のため、__AFL_LOOP を使う永続モードのハーネスをサポートします |
| cargo-fuzz | Rust 固有の fuzz_target! マクロと arbitrary クレートの統合を使用します |
| atheris | Python ハーネスがバイト列を受け取り、Python 関数を呼び出します |
| ossfuzz | クラウドファジング用に、特定のディレクトリ構造に配置されたハーネスが必要です |
関連する手法
| スキル | 関係 |
|---|---|
| coverage-analysis | ハーネスの有効性、つまり対象コードに到達しているかを測定します |
| address-sanitizer | ハーネスが発見したバグ(バッファーオーバーフロー、解放後使用)を検出します |
| fuzzing-dictionary | ハーネスで形式チェックを通過できるよう、ファザーにトークンを提供します |
| fuzzing-obstacles | SUT がハーネスのルール(終了、非決定性)に違反した場合にパッチを適用します |
リソース
主な外部リソース
libFuzzer での入力分割 - Google Fuzzing Docs マジックセパレーターや FuzzedDataProvider の使用など、単一のファジングハーネスで複数の入力パラメーターを処理する手法を説明しています。
Protocol Buffers を使った構造を考慮したファジング カスタムミューテーターとともに protobuf を中間形式として使用し、形式のエンコーディングではなくメッセージの内容が変異するようにする高度な手法です。
libFuzzer のドキュメント ハーネスの要件、ベストプラクティス、高度な機能を網羅した LLVM の公式ドキュメントです。
cargo-fuzz の書籍 cargo-fuzz と arbitrary クレートを使った Rust のファジングハーネスの作成方法を解説する包括的なガイドです。
動画リソース
- 効果的なファイル形式ファジング - ファイル形式パーサー用のハーネス作成に関するカンファレンストーク
- C/C++ プロジェクトの最新ファジング - ハーネス設計パターンを解説するチュートリアル