engineering

LLM誘導型ファジング:より広いカバレッジ、より少ない無意味なクラッシュ

ある火曜日、見栄えのよさそうな SIGSEGV を追いかけて7時間を失ったことがあった。私たちは特定の顧客の JWT パーサーをファズしていて、AFL++ がほぼ即座に再現可能なクラッシュを吐き出した。発見の匂いがした。それを隔離し、afl-tmin で最小化し、PoC を書き、頭の中ですでに CVE を起草している者の自信を持って社内チャンネルに送信した。

クラッシュは空のバッファに対する strncpy だった。なぜなら JWT ヘッダーが文字どおり文字列 { に続いてバイナリバイトだったからだ。パーサーは、何か興味深いものに到達する前に base64url_decode を実行し、本番環境では決して到達しないパスで爆発していた:前段のバランサーは Content-Type とトークン内に少なくとも1つのドットを要求する。バグだった、確かに。役にも立たなかった、確かに。

その午後、私たちはもう十分だと決めた。

なぜグレーボックスファザーは文法で詰まるのか

AFL のミューテーション戦略(ビット反転、スプライシング、算術)は文法盲である:変異させた入力のほとんどはパーサーを通過しない。Wang et al.(Superion、ICSE 2019)はすでに「AFL は構文的正しさへの対処に膨大な時間を費やし、パースエラーしか見つけられない」と指摘していた。

JSON、ASN.1、またはあらゆる状態を持つ形式について、効果的なカバレッジは停滞する。多くのクラッシュが現れるが、ほとんどはプロローグ部分のものである:バリデーター、base64 デコーダー、長さチェック。興味深い部分(署名検証ロジック、kid の処理、algorithm confusion 型攻撃)は、ビット反転では越えられない壁の向こうに座っている。

意味的ミューテーションエンジンとしての LLM

Xia et al. は Fuzz4All(ICSE 2024)[1] でこれを定式化している:LLM は「暗黙的に構文、意味論、有効な API 制約を学習する」。それはもっともらしい入力の確率的生成器である。

Deng et al. は TitanFuzz(ISSTA 2023)[2] で TensorFlow/PyTorch 上で30~50%多いカバレッジを示した。Meng et al. は ChatAFL(NDSS 2024)[3] でこの原理をネットワークプロトコルに適用した:状態遷移が47.6%増、新規 CVE が9件。Yang et al. は WhiteFox(OOPSLA 2024)[4] で、入力要件を導き出すためにソースコードを読むエージェントを追加した。

共通のパターン:LLM はファズしない、候補を提案する。従来のファザーがカバレッジ誘導エンジンであり続ける。

私たちのアーキテクチャ:意味的プラグイン付きの AFL++

Gwaihir CLI は AFL++ をラップし、2つを追加する:ミューテーションの一定割合を意味的プロバイダーに委譲するカスタムミューテーターと、既知の文法、過去のサンプル、同じ形式の以前の CVE を提供する Beorn。

  1. Gwaihir がターゲットを解析する。Beorn が形式を認識した場合、シード文法を注入する。
  2. AFL++ は仕様から LLM が生成した初期コーパスで開始する。
  3. カスタムミューテーターは afl_custom_fuzz にフックする。N 回の反復ごとに LLM に意味的ミューテーションを要求する。
  4. カバレッジを増やした入力はコーパスに再注入される。
// gwaihir_mutator.c — 説明用フラグメント
size_t afl_custom_fuzz(void *data, uint8_t *buf, size_t buf_size,
                       uint8_t **out_buf, uint8_t *add_buf,
                       size_t add_buf_size, size_t max_size) {
  gwaihir_ctx_t *ctx = (gwaihir_ctx_t *)data;
  ctx->counter++;
  if (ctx->counter % ctx->llm_every == 0) {
    return gwaihir_llm_mutate(ctx, buf, buf_size, out_buf, max_size);
  }
  return afl_havoc_mutate(ctx, buf, buf_size, out_buf, max_size);
}

JWT パーサーの事例

同じバイナリ上での24時間キャンペーン:

  • バニラ AFL++:12.3%カバレッジ、47件のユニーククラッシュ、2件が悪用可能。
  • AFL++ + 辞書トークン:19.8%カバレッジ、31件のクラッシュ、3件が悪用可能。
  • AFL++ + Gwaihir/Beorn:41.7%カバレッジ、18件のユニーククラッシュ、7件が悪用可能。

合計クラッシュ数は少ないが、より濃密なクラッシュである。7件中3件は最終的に kid ロジックと非対称アルゴリズムの処理における実際のバグであった。

Fuzz4All と私たちのアプローチ

Fuzz4All はより野心的である:LLM こそが生成ループである。莫大な汎用性(GCC/Clang/Z3/OpenJDK で98件のバグ)だが、2つの制限がある:カバレッジフィードバックなしには盲目的に手探りすることになり、各ミューテーションがモデルへの呼び出しとなる。私たちのアプローチはより控えめで安価である:AFL++ がビット反転速度で98%を処理し、LLM はカバレッジをパーサーの先へ押し進めるためにのみ介入する。

トレードオフ

トークンコスト。24時間 × 1呼び出し/50実行 × 5,000実行/秒 = 約860万呼び出し。積極的なキャッシングとローカルモデルなしでは実行不可能。95%についてはローカル GPU 上の量子化モデルを使用し、救出ミューテーション用に大型モデルを予約している。

レイテンシ。LLM の呼び出し1回、ローカルであっても数十~数百ミリ秒。AFL++ は同期的である。LLM が事前生成されたミューテーションのプールを埋めていく非同期キューを使用する。

仕様不足。曖昧なプロンプト → LLM が退屈な入力を幻覚する。狭いプロンプト → 既知のケースを複製する。プロンプトに対する反復は、コードに対する反復とほぼ同じくらい行う。

私たちが持ち帰るもの

LLM 誘導型ファジングは AFL++ の代替ではない。これは、ターゲットが複雑なパースを持つときにカバレッジの天井を引き上げる補完である。より多くのクラッシュを追うのではない;より良いクラッシュを追う。チームにとって、それは無駄なトリアージに費やす時間を減らし、クライアントにとって重要なバグに費やす時間を増やすことを意味してきた。

参考文献