fuzzing-obstacles について
この技法ガイドでは、本番時の動作を保ちながら、条件付きコンパイルでファジング用に被テストコードを調整する方法を説明します。探索を妨げる検証や非決定的な状態への対処と、誤検出リスクの評価を扱います。
主な機能
- 01検証やグローバル状態の障害を特定
- 02条件付きコンパイルでファジング用ビルドを調整
- 03固定シードで動作を決定的にする
- 04カバレッジ変化を段階的に測定
- 05チェックの迂回による誤検出リスクを評価
活用シーン
- 01チェックサムが深いコードパスへの到達を阻む場合
- 02時刻ベースのシードで実行が不安定な場合
- 03複雑な形式検証で有効入力の生成が困難な場合
- 04カバレッジで広範囲の未到達コードが見つかった場合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trailofbits/ skills --skill fuzzing-obstaclesインストール後、Claude Code で/fuzzing-obstaclesを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlassets/trail-of-bits-mark.svg
詳細
ファジングの障害を克服する
コードベースには、十分なカバレッジを妨げるファジング対策パターンが含まれていることがよくあります。チェックサム、グローバル状態(時刻をシードとする PRNG など)、検証チェックによって、ファザーがより深いコードパスを探索できなくなる場合があります。この手法では、製品版の動作を維持しながら、ファジング中にこれらの障害を回避できるよう、System Under Test(SUT)にパッチを適用する方法を紹介します。
概要
実際のプログラムの多くは、ファジングを考慮して設計されていません。次のような場合があります。
- 入力を処理する前にチェックサムや暗号学的ハッシュを検証する
- グローバル状態(システム時刻、環境変数など)に依存する
- 非決定的な乱数生成器を使用する
- ファザーが有効な入力を生成しにくくなる複雑な検証を行う
これらのパターンがあると、次の理由からファジングが困難になります。
- チェックサム: ファザーは正しいハッシュ値を推測する必要があります(その可能性は天文学的に低いです)
- グローバル状態: 同じ入力でも実行ごとに動作が異なります(決定性が損なわれます)
- 複雑な検証: ファザーが、より深いコードを探索する代わりに検証エラーへの到達に労力を費やします
解決策は条件付きコンパイルです。ファジング用ビルドではコードの動作を変更し、製品版のコードは変更せずに保ちます。
主な概念
| 概念 | 説明 |
|---|---|
| SUT のパッチ適用 | ファジングに適したものにするため、System Under Test を変更すること |
| 条件付きコンパイル | コンパイル時フラグに応じて動作が異なるコード |
| ファジング用ビルドモード | ファジング専用のパッチを有効にする特別なビルド設定 |
| 偽陽性 | ファジング中に見つかったクラッシュのうち、製品版では発生しないもの |
| 決定性 | 同じ入力が常に同じ動作を引き起こすこと(ファジングに不可欠です) |
適用する場合
次の場合にこの手法を適用してください。
- ファザーがチェックサムやハッシュの検証で行き詰まる
- カバレッジレポートで、検証の先にある大きなコードブロックが到達不能と示される
- コードが時刻ベースのシードや、その他の非決定的なグローバル状態を使用している
- 複雑な検証により、有効な入力を生成するのがほぼ不可能である
- ファザーが同じ検証エラーに何度も到達する
次の場合はこの手法を適用しないでください。
- 良質なシードコーパスや辞書で障害を克服できる
- 検証がファザーにとって学習可能なほど単純である(マジックバイトなど)
- 文法ベースまたは構造を認識するファジングを行っており、検証に対処できる
- チェックを省略すると偽陽性が多くなりすぎる
- コードがすでにファジングに適している
クイックリファレンス
| タスク | C/C++ | Rust |
|---|---|---|
| ファジング用ビルドか確認する | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION | cfg!(fuzzing) |
| ファジング中にチェックを省略する | #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION return -1; #endif | if !cfg!(fuzzing) { return Err(...) } |
| よくある障害 | チェックサム、PRNG、時刻ベースのロジック | チェックサム、PRNG、時刻ベースのロジック |
| 対応するファザー | libFuzzer, AFL++, LibAFL, honggfuzz | cargo-fuzz, libFuzzer |
手順
ステップ 1:障害を特定する
ファザーを実行し、カバレッジを分析して到達不能なコードを見つけます。よくあるパターンは次のとおりです。
- より深い処理の前にチェックサムやハッシュを検証している箇所を探す
- システムのシードを使う
rand()、time()、srand()の呼び出しがないか確認する - ほとんどの入力を拒否する検証関数を見つける
- 実行ごとに異なるグローバル状態の初期化を特定する
役立つツール:
- カバレッジレポート(coverage-analysis technique を参照)
-fprofile-instr-generateを使用したプロファイリング- エントリポイントの手動コード検査
ステップ 2:条件付きコンパイルを追加する
ファジング用ビルドで障害を回避できるように、該当箇所を変更します。
C/C++ の例:
// Before: Hard obstacle
if (checksum != expected_hash) {
return -1; // Fuzzer never gets past here
}
// After: Conditional bypass
if (checksum != expected_hash) {
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
return -1; // Only enforced in production
#endif
}
// Fuzzer can now explore code beyond this check
Rust の例:
// Before: Hard obstacle
if checksum != expected_hash {
return Err(MyError::Hash); // Fuzzer never gets past here
}
// After: Conditional bypass
if checksum != expected_hash {
if !cfg!(fuzzing) {
return Err(MyError::Hash); // Only enforced in production
}
}
// Fuzzer can now explore code beyond this check
ステップ 3:カバレッジの改善を確認する
パッチ適用後、次の手順を行います。
- ファジング用インストルメンテーションを有効にして再ビルドする
- ファザーを短時間実行する
- パッチ適用前のバージョンとカバレッジを比較する
- 新しいコードパスが探索されていることを確認する
ステップ 4:誤検知のリスクを評価する
チェックを省略することで、発生し得ないプログラム状態が生じないか検討します。
- チェック後のコードは、検証済みのプロパティを前提としているでしょうか?
- 検証を省略すると、本番環境では起こり得ないクラッシュにつながるでしょうか?
- 暗黙的な状態依存はあるでしょうか?
誤検知が起こりやすい場合は、より対象を絞ったパッチを検討してください(以下の「一般的なパターン」を参照)。
一般的なパターン
パターン:チェックサム検証を回避する
用途: ハッシュ/チェックサムがファザーの進行を妨げている場合
変更前:
uint32_t computed = hash_function(data, size);
if (computed != expected_checksum) {
return ERROR_INVALID_HASH;
}
process_data(data, size);
変更後:
uint32_t computed = hash_function(data, size);
if (computed != expected_checksum) {
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
return ERROR_INVALID_HASH;
#endif
}
process_data(data, size);
誤検知のリスク: 低 — データ処理がチェックサムの正しさに依存しない場合
パターン:PRNG のシードを固定する
用途: 非決定的な乱数状態が再現性を妨げている場合
変更前:
void initialize() {
srand(time(NULL)); // Different seed each run
}
変更後:
void initialize() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
srand(12345); // Fixed seed for fuzzing
#else
srand(time(NULL));
#endif
}
誤検知のリスク: 低 — ファザーは固定シードであらゆるコードパスを探索できます
パターン:慎重に検証を省略する
用途: 検証を省略する必要がある一方で、後続コードに前提条件がある場合
変更前(危険):
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!validate_config(&config)) {
return -1; // Ensures config.x != 0
}
#endif
int32_t result = 100 / config.x; // CRASH: Division by zero in fuzzing!
変更後(安全):
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!validate_config(&config)) {
return -1;
}
#else
// During fuzzing, use safe defaults for failed validation
if (!validate_config(&config)) {
config.x = 1; // Prevent division by zero
config.y = 1;
}
#endif
int32_t result = 100 / config.x; // Safe in both builds
誤検知のリスク: 軽減済み — 検証を省略する代わりに、安全なデフォルト値を設定します
パターン:複雑な形式検証を回避する
用途: 複数段階の検証により、有効な入力の生成がほぼ不可能になる場合
Rust の例:
// Before: Multiple validation stages
pub fn parse_message(data: &[u8]) -> Result<Message, Error> {
validate_magic_bytes(data)?;
validate_structure(data)?;
validate_checksums(data)?;
validate_crypto_signature(data)?;
deserialize_message(data)
}
// After: Skip expensive validation during fuzzing
pub fn parse_message(data: &[u8]) -> Result<Message, Error> {
validate_magic_bytes(data)?; // Keep cheap checks
if !cfg!(fuzzing) {
validate_structure(data)?;
validate_checksums(data)?;
validate_crypto_signature(data)?;
}
deserialize_message(data)
}
誤検知のリスク: 中 — デシリアライズ処理では、不正なデータを適切に処理する必要があります
高度な使い方
ヒントとコツ
| ヒント | 効果 |
|---|---|
| 低コストの検証は維持する | マジックバイトやサイズチェックを使えば、あまりコストをかけずにファザーを導けます |
| PRNG のシードを固定する | あらゆるコードパスを探索しながら、動作を決定的にできます |
| 段階的にパッチを適用する | 障害要因を一度に 1 つずつ取り除き、カバレッジへの影響を測定します |
| 防御的なデフォルト値を追加する | 検証を省略する際は、安全なフォールバック値を設定します |
| すべてのパッチを文書化する | 将来のメンテナーが、ファジング時と本番環境での違いを理解できるようにします |
実例
OpenSSL: FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION を使用して、暗号アルゴリズムの動作を変更します。たとえば、crypto/cmp/cmp_vfy.c では、ファジング中に一部の署名チェックを緩和し、証明書検証ロジックをより深く探索できるようにしています。
ogg crate(Rust): cfg!(fuzzing) を使用して、ファジング中にチェックサム検証を省略します。これにより、ファザーは正しいチェックサムの推測に労力を費やすことなく、オーディオ処理コードを探索できます。
パッチの効果を測定する
パッチの適用後、改善度を定量化します。
- 行カバレッジ:
llvm-covまたはcargo-covを使って、新たに到達可能になった行を確認します - 基本ブロックカバレッジ: 行カバレッジよりも細かく測定できます
- 関数カバレッジ: 新たに到達可能になった関数の数を確認します
- コーパスサイズ: ファザーがより多様な入力を生成するか確認します
効果的なパッチでは、通常、カバレッジが 10〜50%以上向上します。
他の手法との組み合わせ
障害要因へのパッチ適用は、次の手法と相性がよいです。
- コーパスシーディング: 初期解析を通過できる有効な入力を与えます
- 辞書: ファザーがマジックバイトや一般的な値を学習するのに役立ちます
- 構造を考慮したファジング: 複雑な形式には protobuf や文法定義を使用します
- ハーネスの改善: より優れたハーネスを使えば、障害要因を完全に回避できる場合があります
アンチパターン
| アンチパターン | 問題 | 正しいアプローチ |
|---|---|---|
| すべての検証を一律にスキップする | 偽陽性が発生し、ファジングが不安定になります | カバレッジを妨げる特定の障害だけをスキップします |
| リスク評価を行わない | 偽陽性によって時間が浪費され、実際のバグが見落とされます | 後続のコードが前提としている条件を分析します |
| パッチの記録を忘れる | 今後の保守担当者が変更点を理解できません | パッチが安全である理由を説明するコメントを追加します |
| 測定せずにパッチを適用する | 効果があったかどうかわかりません | 適用前後のカバレッジを比較します |
| パッチを適用しすぎる | ファジング用のビルドが本番環境から大きく乖離します | ビルド間の違いを最小限にします |
ツール別のガイダンス
libFuzzer
libFuzzer はコンパイル時に FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION を自動的に定義します。
# C++ compilation
clang++ -g -fsanitize=fuzzer,address -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
harness.cc target.cc -o fuzzer
# The macro is usually defined automatically by -fsanitize=fuzzer
clang++ -g -fsanitize=fuzzer,address harness.cc target.cc -o fuzzer
統合のヒント:
- マクロは自動的に定義されるため、手動での定義は通常不要です
- マクロの有無は
#ifdefで確認します - サニタイザーと組み合わせることで、新たに到達可能になったコードのバグを検出できます
AFL++
AFL++ も、コンパイラーラッパーを使用すると FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION を定義します。
# Compilation with AFL++ wrappers
afl-clang-fast++ -g -fsanitize=address target.cc harness.cc -o fuzzer
# The macro is defined automatically by afl-clang-fast
統合のヒント:
afl-clang-fastまたはafl-clang-ltoを使用すると、マクロが自動的に定義されます- 永続モードのハーネスでは、障害箇所へのパッチ適用が特に効果的です
- 入力から状態への変換をさらに行うには、
AFL_LLVM_LAF_ALLの使用を検討してください
honggfuzz
honggfuzz も、ターゲットのビルド時にこのマクロをサポートします。
# Compilation
hfuzz-clang++ -g -fsanitize=address target.cc harness.cc -o fuzzer
統合のヒント:
hfuzz-clangまたはhfuzz-clang++ラッパーを使用します- 条件付きコンパイルでこのマクロを利用できます
- honggfuzz のフィードバック駆動型ファジングと組み合わせます
cargo-fuzz(Rust)
cargo-fuzz はビルド時に fuzzing cfg オプションを自動的に設定します。
# Build fuzz target (cfg!(fuzzing) is automatically set)
cargo fuzz build fuzz_target_name
# Run fuzz target
cargo fuzz run fuzz_target_name
統合のヒント:
- 本番ビルドでの実行時チェックには
cfg!(fuzzing)を使用します - コンパイル時の条件付きコンパイルには
#[cfg(fuzzing)]を使用します - fuzzing cfg が設定されるのは
cargo fuzzによるビルド時だけで、通常のcargo buildでは設定されません - テスト時には
RUSTFLAGS="--cfg fuzzing"で手動で有効にできます
LibAFL
LibAFL は、C/C++ で記述されたターゲット向けに C/C++ マクロをサポートします。
# Compilation
clang++ -g -fsanitize=address -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
target.cc -c -o target.o
統合のヒント:
- マクロを手動で定義するか、コンパイラーフラグを使用します
- libFuzzer と同じように機能します
- LibAFL ベースのカスタムファザーをビルドする場合に便利です
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| パッチ適用後もカバレッジが向上しない | 障害箇所を誤って特定しています | 実行をプロファイルして、実際のボトルネックを見つけます |
| 偽陽性のクラッシュが多数発生する | 後続のコードが前提としている条件があります | 防御的なデフォルト値または部分的な検証を追加します |
| コードのコンパイル結果が異なる | すべてのビルド設定でマクロが定義されていません | すべてのソースファイルと依存関係でマクロを確認します |
| ファザーがパッチ適用済みのコードでバグを検出する | パッチによって無効な状態が発生しました | 状態の不変条件に照らしてパッチを確認し、より安全な方法を検討します |
| 本番環境のバグを再現できない | ビルド間の違いが大きすぎます | パッチを最小限にし、状態に不可欠なチェックの検証は維持します |
関連スキル
この手法を使用するツール
| スキル | 適用方法 |
|---|---|
| libfuzzer | FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION を自動的に定義します |
| aflpp | コンパイラーラッパーを介してこのマクロをサポートします |
| honggfuzz | 条件付きコンパイルにこのマクロを使用します |
| cargo-fuzz | Rust の条件付きコンパイル用に cfg!(fuzzing) を設定します |
関連する手法
| スキル | 関連性 |
|---|---|
| fuzz-harness-writing | より優れたハーネスによって障害を回避できる場合があります。パッチ適用により、より深い探索が可能になります |
| coverage-analysis | カバレッジを使用して障害を特定し、パッチの効果を測定します |
| corpus-seeding | シードコーパスを使うと、パッチを適用せずに障害を乗り越えられる場合があります |
| dictionary-generation | ディクショナリはマジックバイトには役立ちますが、チェックサムや複雑な検証には役立ちません |
リソース
主な外部リソース
OpenSSL のファジングに関するドキュメント
OpenSSL のファジング基盤は、FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION の大規模な使用例を示しています。このプロジェクトでは、このマクロを使用して暗号検証、証明書の解析、その他のセキュリティ上重要なコードパスを変更し、プロダクション環境での正確性を維持しながら、より深いファジングを可能にしています。
libFuzzer のフラグに関するドキュメント libFuzzer に関する LLVM の公式ドキュメントです。ファザーがコンパイラマクロをどのように定義するか、また、それらを効果的に使用する方法について説明しています。サニタイザーおよびカバレッジ計測との統合についても解説しています。
Rust の cfg 属性リファレンス
cfg!(fuzzing) や cfg!(test) など、Rust の条件付きコンパイルに関する完全なリファレンスです。コンパイル時と実行時の条件付きコンパイルの違いや、ベストプラクティスについて説明しています。