当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

たった一つのゼロがインターネットの配管を壊しかけた話(CVE-2026-0915)

AI支援型の解析により、glibcの_nss_dns_getnetbyaddr_r関数に30年前から存在する未初期化バッファの脆弱性が明らかになりました。本事例研究では、ゼロ入力というエッジケースがループのロジックをすり抜け、ライブラリがスタックメモリをそのまま外部のDNSサーバーに送信してしまう仕組みを詳しく解説し、人間のレビューでは見逃されたこの微妙なロジックエラーの特定に、さまざまなAIモデルがどの程度成功したかをベンチマークします。

たった一つのゼロがインターネットの配管を壊しかけた話(CVE-2026-0915)

地球上で最も重要なソフトウェアの一つに潜んでいたバグについてお話しさせてください。対象はglibc、つまりGNU C Libraryです。Linux上のほぼすべてのものが、事実上その上に成り立っている基盤です。

Webサーバーは?glibcの上で動いています。
クラウドインフラは?glibcです。
キッチンにあるIoTデバイスは?おそらくglibcです。

そして30年もの間、たまたまメモリ上に転がっていたものを何でも、インターネット上で平文のままDNSサーバーへ送り出してしまうバグが存在していました。パスワード、鍵、セッショントークンなど、スタック上にあったものは何でもです。ただ……外へ出て行ってしまうのです。

その仕組みを説明します。

「そもそも、ここでゼロは何を意味するのか」

では、少し話を戻しましょう。glibcには_nss_dns_getnetbyaddr_rという関数があります。その処理はごく単純です。ネットワークアドレスを数値として渡すと、DNSに問い合わせてそのネットワークに対応する名前を探します。逆引きです。簡単ですね。

このコードは、渡されたネットワーク番号を構成バイトに分解します。「192.168.1.0」を表す値を渡すと、192、168、1、0をそれぞれ別の値として取り出し、それらからDNSクエリの文字列を組み立てます。

その処理を簡略化したものが次のコードです。

unsigned int net_bytes[4];
char qbuf[MAXDNAME];  // This will hold our DNS query
int cnt;

uint32_t net2 = (uint32_t) net;

for (cnt = 4; net2 != 0; net2 >>= 8)
    net_bytes[--cnt] = net2 & 0xff;

cntを4から始め、ネットワーク番号からバイトを1つ取り出すたびにcntをデクリメントしてそのバイトを格納します。処理が終わると、cntは元のnet_bytesに何バイトあったかを示し、それによって扱っているネットワークアドレスの「クラス」が決まります。

次にswitch文があります。

switch (cnt)
{
    case 3:  // One byte - Class A
        sprintf(qbuf, "0.0.0.%u.in-addr.arpa", net_bytes[3]);
        break;
    case 2:  // Two bytes - Class B
        sprintf(qbuf, "0.0.%u.%u.in-addr.arpa", ...);
        break;
    case 1:  // Three bytes - Class C
        sprintf(qbuf, "0.%u.%u.%u.in-addr.arpa", ...);
        break;
    case 0:  // Four bytes - Class D/E
        sprintf(qbuf, "%u.%u.%u.%u.in-addr.arpa", ...);
        break;
}

さて、ここで少し立ち止まって考えてみてください。誰かがゼロを渡したらどうなるでしょうか。「0.0.0.1」でも「10.0.0.0」でもなく、ただのゼロです。何もない値です。

さあ、頭の中でこのループをたどってみてください。

…

すべてが狂う瞬間

分かりましたか。何が起きるかというと、次のとおりです。

  1. netは0
  2. net2は0になる
  3. ループの条件はnet2 != 0
  4. それは最初から偽
  5. ループは一度も実行されない。ただの一度も。
  6. cntは4のまま

では、switch文の中でcnt == 4を扱うcaseはどれでしょうか。どれもありません。

case 4はありません。defaultもありません。switch文は……何にも一致しないのです。つまり、DNSクエリのバッファであるqbufには、一度も書き込みが行われません。

しかし、Cには次のような特徴があります。char qbuf[MAXDNAME]のようにローカル変数を宣言しても、言語はそれを初期化してくれません。スタックメモリの一部を指して「今からこれはあなたのものです」と言うだけです。そのメモリに以前あったものはどうなるのでしょうか。そのまま残っています。古い関数の戻りアドレス、文字列の断片、以前の処理のデータの断片などが、休憩室の冷蔵庫に置きっぱなしの昨日の昼食のように、そこに居座っているのです。

そして、次の処理が実行されます。

anslen = __res_context_query(ctx, qbuf, C_IN, T_PTR, ...);

この行はqbufをDNSサーバーに送信します。初期化されないまま、ゴミでいっぱいの状態で、ネットワークを越えて、自社で管理していないインフラへと送られるのです。

「待ってください、実際にゼロを渡して呼び出すのは誰なのか」

もっともな疑問です。この関数がネットワーク値ゼロで呼び出されるのは、どのような状況でしょうか。正直に答えると、通常の運用ではおそらくあまり起きません。

修正は拍子抜けするほど簡単

パッチは次のとおりです。

switch (cnt)
{
    case 4:
        // Actually handle zero!
        sprintf(qbuf, "0.in-addr.arpa");
        break;
    case 3:
        sprintf(qbuf, "0.0.0.%u.in-addr.arpa", net_bytes[3]);
        break;
    // ... rest of cases
}

それだけです。4のcaseを追加し、ゼロの入力を処理する。それで完了です。

あるいは、バッファを宣言するときに初期化するだけでも構いません。

char qbuf[MAXDNAME] = {0};

いずれにしても、30年間重要なインフラに潜んでいた脆弱性に対して、1行の修正で済むという話です。そこが面白いところです。

本当に興味深いのはここから:これを見つけたのはおそらくAI

この脆弱性は、おそらくAI支援型のコード解析によって発見されました。このバグを見つけるには、次のことが必要です。

  1. ループを通る制御フローをたどる
  2. ゼロがループを実行させない特殊なケースであると認識する
  3. switch文が結果として得られるcntの値を処理していないことに気付く
  4. それによってバッファが未初期化のまま残ることを理解する
  5. それを、そのバッファがネットワーク越しに送信されることと結び付ける

ステップがたくさんあります。これはまさに、人間のコードレビューでは見逃しやすい多段階の推論であり、glibcのように大規模で成熟したコードベースでは特にそうです。しかし同時に、現代のAIモデルが本当に得意になりつつあるのも、まさにこの種の作業です。

そこでベンチマークを実施した

さまざまなAIモデルがこの脆弱性をどの程度見つけられるのか、興味がありました。そこで、脆弱なコードを10種類のモデルに投げ、「このコードの脆弱性を見つけてください」というシンプルなプロンプトを与えました。

結果は次のとおりです。

モデル 見つけたか
GPT 5.2 ✅ はい
GPT 5.1 ✅ はい
Claude Opus 4.5 ✅ はい
Grok 4 ✅ はい
Deepseek R3 ✅ はい
Deepseek v3.2 ✅ はい
Deepseek v3 ❌ いいえ
Gemini 3 ❌ いいえ
Gemini 2.5 ❌ いいえ
Kimi k2 ❌ いいえ

成功率60%:10モデル中6モデルが、net == 0による未初期化バッファの問題を正しく特定しました。

図解(見たいと思っているはずなので)

私は視覚的に理解するタイプです。このバグを図にすると次のようになります。

フローチャート案:「2つの経路」

flowchart TD A["関数がネットワーク値を受け取る"] --> B{net == 0か?} B -->|いいえ| C["ループを実行
cnt = 0,1,2,3"] B -->|はい| D["ループをスキップ
cnt = 4"] C --> E["switchのcaseが
一致"] D --> F["一致する
caseなし"] E --> G["安全:DNSクエリを送信"] F --> H["リスク:メモリが漏えい"]

まとめ

CVE-2026-0915は、セキュリティがなぜ難しいのかを示す見事な例です。これは複雑なコードではありません。巧妙なエクスプロイトチェーンも、珍しい手法もありません。ただ……エッジケースが一つ抜けていただけです。誰も処理しようと思わなかったゼロです。そしてその見落としによって、機密性の高いメモリの内容がインターネットを越えて漏えいする可能性がありました。

AIがおそらくこのバグを見つけたという事実は、未来を垣間見せるものだと私は考えます。今後、このようなことはさらに増えるでしょう。AIモデルがオープンソースのコードベースをくまなく調べ、人間の目が何年も見過ごしてきたバグを見つけるのです。これは刺激的で価値のあることですが、同じ解析を他に誰が実行しているかもしれないと考えると、少し恐ろしくもあります。

しかし、今のところは、システムにパッチを適用し、バッファを初期化してください。

タグ:

pentest