
2026/10/28 18:46
AFL++ を活用したより効果的なファスティングテストのためのヒント
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
6 年間の約 1,000 プロジェクトのテストに基づき、著者は AFL++ が Linux 上の C/C++ fuzzing プラットフォームとして決定版であると主張し、ファジー化されていないソフトウェアにはほぼ間違いなくバグが含まれていることを確認しています。速度を損なうことなく脆弱性の検出を最大化するためには、公式文書で強調されている特定のハイパフォーマンス構成を採用すべきです。主要な最適化策には、クラッシュ間でプログラムの状態を維持するパシストンモードの利用(通常は 10 倍ほど高速化)、
afl-gcc-fast とともに -fsanitize=address,undefined および他のフラグでサニタイザーを設定することが含まれます。具体的なチューニングも不可欠であり、デフォルトの 4KiB リミットを超えて max_malloc_fill_size を増やすことで use-after-free エラーを検出し、detect_stack_use_after_return を有効化し、GCC の libstdc++ で -D_GLIBCXX_DEBUG を用いてコンテナオーバーフローを検出します。さらに、単一の翻訳単位(instrumented target と uninstrumented library)でビルドすることで静的関数の設定を簡略化でき、テスト設計ではグローバル状態の干渉を最小限に抑え、ASan による読み取りミスを防ぐために直接バッファ渡しを避け、ファイル入力は memfd_create を使用する必要があります。これらのターゲット指向な調整により、デフォルト設定で見逃されがちな重要なメモリ安全性の問題を発見でき、大規模な fuzzing キャンペーンの徹底的さを大幅に向上させます。本文
ファジングテストで見落とされる不具合を発見するための実践ガイド
ファジングテストはソフトウェアの不具合を機械的に発見するために極めて効果的ですが、実際には軽視され、十分に利用されていないのが現状です。複雑な入力を安定的に受け入れる必要があるプログラムにおいて、もしファジングが行われていない場合、少なくとも 1 つの不具合が浮き彫りにされる可能性が極めて高いです。
過去 6 年にわたり約 1,000 プロジェクトのファジングを行った経験から、**「低く垂れ下がった果実(容易に入手できる不具合)」**を見つけるためのヒントをまとめました。
本稿では、ソースコードが利用可能な C/C++ ターゲットに対し、特に glibc ベースの Linux 配布版上で動作する AFL++ の使い方について解説します。これらのテクニックは既存のファジングテストでも適用可能であり、以前見逃された不具合を発見できる可能性があります。
(1) センサイザーとアサーションの強化
アサーションを増やすことはより効果的なファジングにつながります。Address Sanitizer(ASan) と Undefined Behavior Sanitizer(UBSan) を併用することを強く推奨します。
環境設定とコンパイルオプション
デフォルトの設定では不十分なため、以下のオプションを適切に設定してください。
afl-gcc-fast -g3 -fsanitize=address,undefined ...
ASan のカスタマイズ
ASAN_OPTIONS 環境変数で以下のように設定し、感度を調整します。デバッグ時には symbolize=0 を除いてデフォルトに近い状態にします。
export ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:symbolize=0"
特定の不具合を検出するための追加設定
ファジング速度は若干低下しますが、以下のバグ検出能力の向上には十分な価値があります。
-
use-after-return(戻り後の使用)バグ:
ASAN_OPTIONS+=":detect_stack_use_after_return=1" -
use-after-free バグおよび全アロケーションのパターン埋め込み: デフォルトでは 4KiB 分のみパターンが埋められ、それ以降の未初期化メモリ(例:
のような文字列操作)でのバグが見逃されることがあります。これを防ぐには以下のように設定します。strlenASAN_OPTIONS+=":max_malloc_fill_size=$((1<<30))"(注:アロケーション全体をパターンで埋めることで、永続モード (
) におけるテストの分離性が向上します)persistent mode
C++ の特別なケース
GCC の
libstdc++ とリンクする C++ をファジングする際は、追加のアサーションによりコンテナの境界外アクセスを検出するために、-D_GLIBCXX_DEBUG オプションを検討してください。これは ABI を変更するため注意が必要ですが、ファジングテストでは不整合が瞬時に浮き彫りになるため問題にはなりにくいです。
(2) 永続モード(Persistent Mode)を優先する
AFL++ はコードを書かずにプログラムを
in-place でファジングすることも可能ですが、より強く推奨されるのは 永続モード です。
- 通常のファジングに比べて約 10 倍高速。
- 実装には
またはafl-gcc-fast
を使用します。afl-clang-fast
自作テンプレート fuzztmpl.c
の活用
fuzztmpl.c公式テンプレートには重大な欠陥があるため、以下の自定義テンプレートを推奨します(Vim のバッファ等に読み込んで調整):
#include <unistd.h> __AFL_FUZZ_INIT(); int main(void) { __AFL_INIT(); char *src = 0; unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF; while (__AFL_LOOP(10000)) { int len = __AFL_FUZZ_TESTCASE_LEN; src = realloc(src, len); memcpy(src, buf, len); // ... send src to target ... } }
重要: 元のテンプレートでは
buf と len の扱いに不明瞭な制約があるため、本テンプレートの修正版を使用してください。
ソースコードの取り込み戦略
- 翻訳ユニット(TU)の最小化: 21 世紀には 16 ビットマシンは存在しません。最大で 2 つの TU(計測済みコード用・未計測コード用)のみが必要で、後者は通常ライブラリとして扱います。
- 効率的な include 構成:
#include "src/utils.c" #include "src/parser.c" #include <unistd.h> // 他のソースが明示的に include しなければ見えません
このシンプルテクニックはビルド設定を大幅に簡素化します。
内部関数のテストとハサミ技法
プログラムの任意の関数(静的関数含む)を対象にファジング可能です。外部ヘッダーを含めずに、問題のあるシンボルのみを差し替える「ハサミ技法」で元のソースコードを変更せずにテストできます。
#define main oldmain # include "src/utils.c" # include "src/parser.c" #undef main #include <unistd.h>
これにより、ASan でコンパイルし、「この 50 バイトのファイルを開くとクラッシュ」といった簡潔なバグレポートを作成できます。
(4) ファジングテスト間の相互孤立化
テストは前のテストの影響を受けないべきです。永続モードではグローバル状態の残存が課題となります。
リセットすべき項目
- グローバル変数(例:
の内部バッファなど)。strtok - 未初期化のヒープメモリ。
- ヒープアドレス(テスト間で変動するため、依存すると再現性が損なわれます)。
対策:
- 未初期化メモリはゼロまたはパターンで埋める。
- グローバル変数の使用を避けることリストに「ファジングテスト用」を追加する。
(5) テストバッファを直接渡さない(Critical Fix)
buf と len をそのままターゲット関数に渡すことは最も一般的な間違いです。AFL++ が割り当てる下位バッファは len より大きく、ASan は入力超過読み込みを検出できないため危険です。
正しくはサイズに合ったコピーを使用
// BAD! myprogram(buf, len); // GOOD: サイズに合わせたコピーを作成 char *src = ...; // ここでは適切なサイズのコピーを確保 myprogram(src, len);
- ヌル終端が必要な場合: 必ずヌルバイトを追加する。
- 所有権を持つ型(
など): コピーではなく参照渡し可能だが、ファジング用コピーを作る方が安全。std::string - 所有権のない型(
など): 自分自身のサイズに合ったコピーを作成する必要がある。std::string_view
変更を加え、新たな発見があるか試みてください。
(6) メモリの解放は気にしない
ファジングテスト内でデストラクタの実行を省略し、メモリのみを管理するのが一般的です。
- 理由: AFL++ はターゲットプロセスをリセットするため、メモリ不足になることはほぼありえません。
- 利点: 未初期化メモリの汚染を防ぎ、テスト間の分離性を向上させます。
例外事項
- デストラクタもファジング対象にしたい場合:テスト内で使用する。
- ファイルディスクリプタなどの他のリソースを使い尽くすリスクがある場合:クリーンアップが必要な場合があります。
- アリーナアロケーションを使用している場合:解放を気にする必要はありません(アドレスが安定するため)。
(7) メモリアルファイルディスクリプタによる名目パス対策
多くの悪質なインターフェースは「ファイル名」のみを受け付け、自らを開くことを強いることがあります。並列ファジングや SSD の摩耗を考慮し、メモリアルファイルディスクリプタ(
) を使用します。memfd_create
実装例
int fd = memfd_create("fuzz", 0); assert(fd == 3); // ファイルディスクリプタが 3 に割り当てられるように仮定 while (...) { ftruncate(fd, 0); pwrite(fd, buf, len, 0); // /proc/self/fd/3 を名目パスとして使用 myprogram("/proc/self/fd/3"); }
を使用することで、オフセットをファイル先頭(0)に固定できます。pwrite- ファイルはプロセス固有のため、並列ファジングにも対応可能です。
ディレクトリオブジェクトへの対応
- 直接パス渡し:
myprogram(fd); - FILE 型を受け付ける場合*:
パスではなく、/proc
を使用して作成し、fdmemopen
の心配をせずに渡します。fclosemyprogram(fdmemopen(buf, len, "rb"));
(8) ターゲットのバッファサイズを削減する
「寛容な」巨大な固定バッファ(例:65536 バイト)はバグを隠す原因となります。最悪ケース(オーバーフロー)への到達時間を増やしすぎないよう、バッファサイズを大幅に削減してください。
- 推奨動作: 最大値(65536 など)から 16 など非常に小さなサイズに変更する実験を行ってください。
- メリット: 短いバッファでクラッシュを見つけた場合、入力拡張して元のサイズでもクラッシュさせるのが容易です。これにより、入力の特性をより深く探索できます。
結論とサンプル
上記のテクニックは、本来気づかれなかった不具合を発見するために役立ちます。さらに望ましいのは、これらのファジング技術への認識が、ソフトウェア開発初期段階でのバグ防止に寄与することです。
- テンプレート:
を活用し、堅牢なツールを活用してください。fuzztmpl.c - スピード: これらのノウハウにより数分でファジングテストを構築できます。
- 注意点: 手軽さゆえに慎重さを欠き、結果の記録・カタログ化がおろそかになる傾向があるため注意が必要です。
数千プロジェクトのファジングを通じて蓄積されたこれらの知見は、すぐに活用できる資産となります。