Monday, January 26, 2026

社内勉強会をきっかけにハイパーバイザの脆弱性を見つけた話

 リチェルカセキュリティの師尾です。

私は普段の業務でハイパーバイザの研究開発に携わっていますが、攻撃の専門家ではありません。そんな私が「社内の勉強会に参加したついでに、ハイパーバイザの脆弱性を2件発見してCVEを取得することになった」と言うと驚かれるかもしれません。しかし、社内の勉強会を通じて攻撃者の視点を学び、さらに生成AIを活用した調査手法を取り入れたことで、いち開発者としては気づきにくい脆弱性を発見することができました。

この記事では、勉強会での学びと生成AIを組み合わせて、BitVisorという国産ハイパーバイザから脆弱性を発見し、CVE-2025-61553CVE-2025-61554として報告するまでの過程を紹介します。ちなみに、この取り組みで得られた知見は、弊社の生成AIを使った脆弱性診断サービスの強化にも寄与しています。

社内勉強会について

弊社では、有志メンバーが週次で「pwn勉強会」を開催しています。pwnとはセキュリティ競技であるCTF(Capture The Flag)のいちジャンルで、脆弱性の発見・分析・悪用を扱う競技分野です。CTFの世界はプロゲーマーの世界に近く、弊社には日本トップクラスの現役プレイヤーが在籍しています。彼らはHKCERT(香港)、DEF CON(米国)、CODEGATE(韓国)、SAS(タイ)、snakeCTF(イタリア)といった世界中のCTF大会で予選を勝ち抜き、決勝に出場してきた実績を持っています。

この勉強会のメリットは、元々CTFプレイヤーではないメンバーも、トッププレイヤーたちの指導を通じて攻撃ノウハウを学べることです。いわば「プロが促成栽培されている」環境にあり、私のような開発者が攻撃者の視点を学ぶ絶好の機会となっています。

ある週の勉強会では、VM escape(仮想マシンからホストOSへの脱出)をテーマにした演習を行いました。VM escapeでは、ハイパーバイザ上で動くゲストから、ハイパーバイザの脆弱性を利用してホストに脱出し、ゲスト内からホストのシェルを取る、というのが目標になります。題材としてはバグのあるQEMUデバイスが用意されることが多いようです。

動機となった題材

今回ハイパーバイザの脆弱性を実際に見つける強い動機となった問題が一つあります。それはKalmarCTF 2025で出題された「KalmarVM」という問題です。これは、kvmtoolという小さいながらも比較的よく知られたKVMを利用したハイパーバイザの0-dayを使って解くことが想定解のVM escapeの問題です。

kvmtoolでは、エミュレートするデバイスがほぼすべてVirtIOで実装されています。VirtIOは準仮想化デバイスの標準規格で、PCIなどのバスを通してホストとゲストがデータをやりとりしてデバイスとして動作するように作られています。VirtIOはQEMUやFirecracker, crosvm, Cloud Hypervisorなどのモダンなハイパーバイザのほとんどで実装されています。これは、VirtIOの規格がエミュレータを実装しやすく、パフォーマンスが出やすいように都合よく作られており、かつLinuxなどのOSのサポートも充実しているためです。

この問題では、kvmtoolのVirtIO MMIO handlerの実装にOut-Of-Bounds (OOB) accessのバグがあり、かつvirtio-balloonというVirtIOデバイス実装のメモリレイアウトがそのOOB先の領域として非常に都合が良いということを利用します。利用するバグが提示されているなど、だいぶお膳立てされた状態ではありますが、私も勉強会のなかでexploitを書き、ホストのシェルが取れました。

この問題で印象に残ったこととして、

  1. 比較的有名なハイパーバイザでも未修正の典型的なメモリ由来の脆弱性が存在する
  2. VirtIOというよく知られたデバイスのエミュレータ実装であっても、MMIO handlerというコア部分の実装にバグが存在する

という点がありました。

特に、OOB accessの脆弱性は誰でも作り込んでしまう可能性があり、私が開発しているハイパーバイザでも似たような脆弱性を抱えている可能性もありました。このようなことから、「こうしたVirtIOデバイス実装の脆弱性が、他の実プロダクトでも存在するのではないか」と思うようになりました。

そこで、休日を活用してVirtIOデバイス実装の脆弱性を探すことにしました。

ターゲットの選定

VirtIOデバイスを実装しているプロダクトは数多くありますが、今回はBitVisorを選択しました。これには私が触ったことがありコードベースに慣れているというのもありますが、

  1. 設計思想的にattack surfaceが少なそうなので、脆弱性を見つけるとインパクトがある
  2. KVMを使ったものよりはescapeした先の権限レベルが上位なので違った面白さがありそう

という二つの理由で選びました。

BitVisorは2009年より開発が開始された国産ハイパーバイザで、「ハイパーバイザ自身はほとんど何もしない」という特徴があります。このため、初期の設計思想ではこのようなデバイスエミュレーション起因の脆弱性はほぼなく、attack surfaceが少ないはずです。現在では「ホストの物理NICを隠蔽し、ゲストにはvirtio-netとして見せる」機能があるため、VirtIOデバイス実装が存在します。このような「セキュアであること」を設計思想に据えたハイパーバイザで脆弱性を見つけることができればインパクトが大きいと判断しました。

また、2)は魅力的で、KVMを使ったハイパーバイザは、デバイスエミュレーションがホストユーザ空間で行われているため、デバイス実装の脆弱性を使ってVM escapeしたとしてもホストのカーネル空間まで到達するにはホストカーネルの脆弱性を別途見つけなければなりません。一方、BitVisorではデバイスエミュレーションがホストカーネル空間の単一メモリ空間で行われているため、一度VM escapeできればホストシステム全体に干渉できます。

調査の方針

冒頭で紹介したように、今回の調査ではLLMを活用しました。その動機としては「LLMを利用してツールなしにプロンプトのみを利用してLinux kernelのSMBの脆弱性を見つけることができた」という記事を読んだことです。

この記事では、当時(2025年4月)リリースされたOpenAIのChatGPT o3に対して、脆弱性を見つけるように作り込んだプロンプトとコードを渡し、回答を複数回得るなかで回答の確度を高めていく、というのが主要なアプローチです。

もう一つ重要なポイントとして、Model Context Protocol (MCP) などのツール利用をしていないという点があります。これは、ChatGPTなどのLLMサービスのチャットインターフェースのみを用意できればよい、という大きな利点があります。特に、元の記事にあるような複数のLLMサービスを利用してクロスチェックする場合でも、用意したプロンプトのみを与えるだけで結果が得られるという手軽さがあります。

また、https://arxiv.org/abs/2505.06120の論文にあるように、LLMは複数回やりとりを行うと回答の精度が落ちるという話もあったため、初回のプロンプトで全てのコンテキストを与えるようにしました。

今回は大まかにはこの記事の方針を利用しました。プロンプトを与えるのは初回のみなので、そのプロンプトをいかに作り込むかが重要なポイントとなってきます。

プロンプトは以下の構成となっています。

  1. LLMに与える指示。よくあるプロンプトエンジニアリングの手法を利用しています
  2. 与えられるソースコードの文脈。次に与えられるソースコードが何のソフトウェアの何のコンポーネントのものなのか、それがどのように他のコンポーネントと関係しており、典型的な脆弱性がある場合には、どのような場合があるのか、など人間が持っているドメイン知識を可能な限り与えます
  3. 実際のソースコード。ソースコードは文脈が明らかになるように、一部分ではなく関係するほぼ全てのソースコードを与えます。コードの依存関係からソースファイルは複数になっているので、files-to-promptといった自動でソースコードをLLMが解釈しやすいXML風の記述のプレーンテキスト化するツールを利用します。経験上、こうした「データ構造っぽいもの」をLLMに与える場合、JSONよりもXML風の方がより正しく解釈してくれる可能性が高い気がします

今回作成したプロンプトは1)の指示文についてはSMBの記事のものをベースに改変し、2)は新たに人間が作成しました。こちらはLLMによって事前に作成させることも可能だと思います。

得られた各テキストを統合して一つのプロンプトとします。 https://gist.github.com/retrage/70336b8d276b933563b234cd785f411d に実際に作成したプロンプトを示します。

今回はChatGPT o3 ThinkingとGemini 2.5 Proに複数回このプロンプトを与えたうえでクロスチェックを行いました。

確度の高いバグとトリアージ

広く知られているように、現在のLLMは同じプロンプトに対する回答が毎回異なります。このため一貫しないLLMの返答を比較する必要があります。こうした比較もLLM自体にさせるということもできますが、今回は個人的にどれぐらいの回答のばらつきや精度の違いがあるのかが気になっていたため手動で行うことにしました。

得られた確度の高いバグの指摘は以下の三つでした:

  • do_net_ctrl でのHeap Buffer Overflow
  • queue_size == 0 としたときのゼロ除算
  • VIRTIO_NET_CTRL_MAC_TABLE_SET でのサイズチェック不足によるOOB Read

この他にも複数回出てきた指摘もありますが、偽陽性だったため除外してあります。

これらのバグの検証については、検証環境にBitVisorのvirtio-netがバックエンドとしてサポートしている物理NICが必要だったため、実機を用いて行いました。今回は手元にあった私物のIntel NUC (NUC8i3BEH) を利用しました。なお、virtio-netはオプショナルなためビルド前にコンフィグで有効にして、必要な設定を行ってからビルドしました。

脆弱性の報告とパッチの作成

報告すべきバグが見つかりその妥当性も検証できたところで開発元に報告します。よくある流れとしては、非公開に開発元に報告し、開発元が確認しパッチを作成して脆弱性情報を公開する、というものです。

今回の調査と前後して、有名なOSSで脆弱性報告にまつわる問題が表面化していました。例えば、libxml2のボランティアのメンテナが大量の脆弱性報告の対応に疲弊しこれ以上の脆弱性報告を受け付けないとしたことが一時話題になりました。普段は開発者の立場である私としては、こうした話を念頭に脆弱性報告を行いました。

規模が大きく大企業が資本を投資しているような体力があるOSSの場合は脆弱性報告の窓口が用意されていることが多いですが、BitVisorは開発が活発であるものの、比較的こぢんまりとした開発コミュニティであり、セキュリティの窓口も用意されていない状態です。また、バグとしてもBitVisor全体に大きな影響があるものであれば別ですが、今回のvirtio-netはビルド時に有効化していない限りは影響がないものであり、そこまで重要度が高くないと判断しました。そのため、公開の通常の開発者メーリングリストにセキュリティ窓口についての相談をしてから個別に開発者の方とメールにてやりとりをしました。

メールでは、セキュリティ窓口が用意されていないとのことでしたのでlibxml2での事例を踏まえ、脆弱性の修正パッチは特別扱いせずに、通常のバグ修正と同様に公開の場でパッチを取り込んでもらうことになりました。

こちらとしては開発者の方を煩わせたくなかったため、こちらで作成した修正パッチをバグの説明とともに送付しました。何回かのやりとりと修正ののち、パッチを取り込んでいただきました。

見つかったバグをそれぞれ紹介していきます。VirtIOの仕様などやや込み入った話になるので、読み飛ばしてもらっても構いません。

do_net_ctrl でのHeap Buffer Overflow

LLMからの回答の一つを抜粋して紹介します。

The intended logic:

  1. First loop computes total command length len by summing desc[d].len.
  2. If len <= PAGESIZE, allocate cmd = alloc(len).
  3. Second loop copies each descriptor’s payload into cmd, doing:
if ((copied + desc_len > len) || (last && copied + desc_len != len)) {

printf("... strange ...\n");

}

memcpy(cmd + copied, buf_ring, desc_len);

copied += desc_len;

The problem

The check for inconsistent descriptor lengths only prints a debug message; it does not abort or clamp the copy.

So if any desc_len > len (or sum of them is inconsistent), the code still does:

memcpy(cmd + copied, buf_ring, desc_len);

with cmd having only len bytes allocated.

Because both len and desc_len are 32-bit u32, the attacker can also use integer overflow in the first pass to keep len small while one of the descriptor lengths is huge.

do_net_ctrl では、virtio-netのcontrolqというVirtIO Queueに来たリクエストを処理します。この関数のなかで、ゲストから渡されたVirtIO Descriptorに指定されているゲストメモリ上のデータをホストメモリにコピーします。VirtIO Descriptorを複数同時に渡すことで、ゲストメモリのデータを複数同時に指定できます。この関数では、与えられたVirtIO Descriptorを走査し、全体でコピーするのに必要なバッファサイズをu32の len で計算します。len だけのバッファを確保してから、 各Descriptorについてバッファに memcpy します。ここでの指摘は、 len が Descriptorのサイズ desc_len よりも小さい場合に処理を止めていないために、 len がInteger Overflowした場合に範囲外書き込みが起きるというものです。例えばDescriptorを二つ用意し、1つ目に 0xFFFFF000 、2つ目に 0x200 を与えると len は 0x100 となり 0x100 の大きさのバッファに対して 0xFFFFF000 のデータがコピーされてしまいます。実際、同様のPoCを書いてゲストで実行したところ、ホストのBitVisorを含めてシステムがハングしました。PoCは https://github.com/retrage/advisories/blob/main/CVE-2025-61553/poc.c にあります。この脆弱性は CVE-2025-61553 が採番されました。

こちらの脆弱性は https://sourceforge.net/p/bitvisor/code/ci/48090711ade5910b601d2b0034859a6a890cc82d/ で修正されました。

queue_size == 0 としたときのゼロ除算

次はVirtIO Queueのサイズ操作に関するバグです。LLMからの回答を以下に示します。

All three queue handlers do:

idx_u = used->idx % vnet->queue_size[q]; // q = 0,1,2 depending on queue

Since ccfg_queue_size allows arbitrary data->word (including 0) and there is no constraint tying queue enabling to a non-zero size, a malicious guest can:

  1. Set queue_size[n] = 0.
  2. Set queue_enable[n] = 1.
  3. Notify that queue via queue_notify.

When the corresponding handler (virtio_net_sendvirtio_net_recv, or virtio_net_ctrl) runs, used->idx % vnet->queue_size[n] is a modulo by zero, which is undefined behaviour in C and typically results in a hypervisor crash.

これは CVE-2025-61554 が採番されたものですが、見ての通り非常に簡単なゼロ除算のバグです。該当箇所は以下の通りになっています。

static void
ccfg_queue_size (struct virtio_net *vnet, bool wr, union mem *data,
		 const void *extra_info)
{
	u16 n = vnet->selected_queue;
	if (n >= VIRTIO_N_QUEUES)
		return;
	if (wr) {
		vnet->queue_size[n] = data->word;
		if (vnet->queue_size[n] != VIRTIO_NET_QUEUE_SIZE)
			printf ("virtio_net: queue %u size is %u\n",
				n, vnet->queue_size[n]);
	} else {
		data->word = vnet->queue_size[n];
	}
}

if (vnet->queue_size[n] != VIRTIO_NET_QUEUE_SIZE) でキューのサイズチェックはあるものの、printfするだけで処理が続行されてしまう状態でした。これはLLMの指摘は正しく、実際にPoCを書いてみると確かにゼロ除算が起き、ゲストからホストをハングさせることができました。作成したPoCは https://github.com/retrage/advisories/blob/c20246f93edac1b8ad3c8c5e3e768d02405850c7/CVE-2025-61554/poc.c にあります。

こちらの脆弱性は https://sourceforge.net/p/bitvisor/code/ci/de84887f4418fcd67945b4aa4842e035bce0dfa9として修正がされました。

VIRTIO_NET_CTRL_MAC_TABLE_SET でのサイズチェック不足によるOOB Read

こちらは脆弱性とまでは言えないバグだったためCVE採番申請は行いませんでしたが、少し面白いと思ったので紹介します。

File / function: drivers/net/virtio_net.c :: process_ctrl_mac_cmd()

uni_n_entries   = *(u32 *)&cmd[2];                          // ①
…
multi_n_entries = *(u32 *)&cmd[2 + 4 + uni_table_size];     // ②
…
memcpy (vnet->unicast_filter,  c, uni_table_size);          // ③
memcpy (vnet->multicast_filter, c, multi_table_size);       // ④
  • Only the very first 11 bytes are validated (cmd_size < 11 test).
  • The routine never checks that the calculated offsets2+4+uni_table_size or …+4+multi_table_size are still inside the received buffer.
  • A guest can advertise cmd_size = 11, set uni_n_entries = 16 and multi_n_entries = 16uni_table_size = 96, so the reads/writes in(②-④) access ~180 bytes beyond the cmd heap buffer that was allocated from user-controlled data in do_net_ctrl().
  • Although the destination arrays are large enough, the source read crosses the heap boundary, causing:
    • host crash (SIGSEGV on hardened builds),
    • or sensitive heap data disclosure if the attacker inspects the overwrittenfilter arrays via subsequent control commands.

virtio-netには多くのoptionalな機能があり、controlqというVirtIO Queueにゲストからコマンドを送ることで操作できます。コマンドは以下のフォーマットを持ちます。

struct virtio_net_ctrl { 
        u8 class; 
        u8 command; 
        u8 command-specific-data[]; 
        u8 ack; 
} __attribute__((packed)); 

class と command で送るコマンドの種類を指定し、command-specific-data でコマンド固有のデータを持ちます。 ack はその操作の実行結果をデバイスが書き込むフィールドです。

BitVisorのvirtio-net実装では VIRTIO_NET_CTRL_MAC_TABLE_SET に対応しており、受信するパケットのMACアドレスベースのフィルタリングができます。ゲストのドライバはcontrolqに対して以下の可変長の構造体のデータを持つコマンドを command-specific-data としてフィルタするMACアドレスを指定できます。

struct virtio_net_ctrl_mac { 
        le32 entries; 
        u8 macs[entries][6]; 
} __attribute__((packed));

最初の entries でエントリ数を指定し、 macs でそのエントリ数分のMACアドレスが配置されるような形をしています。送信するコマンドでは、ユニキャストとマルチキャストでそれぞれこの構造体が2つある前提となります。すべてのエントリ数が0の場合コマンド全体のサイズは、

class (1 byte) + command (1 byte) + entries (4 bytes) + entries (4 bytes) + ack (1 byte) = 11 bytes

となり、コマンドのサイズは11 bytes以上でなければなりません。

さて、BitVisorのコードを見るとコマンドの最小サイズの11 bytes以上であることはチェックしています。しかし、 entries で指定された数のMACアドレス分のデータが与えられているかはチェックせずにバッファにコピーしています。前半のユニキャストを最大の16エントリとし、後半のマルチキャストを0とした分のサイズのバッファを与えたうえで、各 entries は 最大の16を指定すると、マルチキャストのMACアドレス分だけOOB Readした結果が vnet->multicast_filter に書き込まれます。BitVisorは単一のメモリ空間を使っているため、OOB Readした部分にはBitVisorが他の用途で利用しているデータが入っています。

幸いなことにvirtio-netには設定したパケットフィルタのMACアドレスをゲストから読み出す方法が用意されていないため、これをゲスト側で読み出してInformation Leakさせることはできませんでしたが、このようなチェック漏れは自身でもやってしまいそうであり、身近に感じたため紹介しました。こちらは https://sourceforge.net/p/bitvisor/code/ci/b58daea983a1f9231845a9b23cf6b5caf50e70ce/ として提案した修正が取り込まれました。

思うこと、得られたもの

LLM自体の性能について

ハイパーバイザやVirtIOについて一般的な知識と理解があるという前提において、十分なコンテキストを与えさえすればLLMが今回のような脆弱性を指摘できるというのは先の記事から考えれば当然のことでした。一方で、現在では改善されていると思いますが、試した2025年5月時点では、テンプレートを渡したうえでLLMにPoCを書かせてもクオリティにばらつきがあり、結局PoCのポン出しは諦めたということがありました。

今回この活動を通して得られた思わぬ収穫として、作成したプロンプトによってLLMの性能を手軽に試すことができるようになったということがあります。自分が検証でき、興味がある分野での現実的なセキュリティレビューのタスクでツールも不要なので、新しいLLMのモデルが出たときにはそのまま与えてみて実用するうえでの性能を試すことができ便利です。

LLMがバグハントでできること、できないこと

今回は初期調査ではLLMを活用したものの、その後の検証では物理的に人が介入して作業する場面が多いという印象でした。特に今回のような実ハードウェアを検証に使う場合、検証環境のセットアップや環境に合わせたビルド、マシンがハングしたときの再起動など現状では人手の介入が必須な場面が数多くありました。もちろんこうした部分もKVM switchとVLMを組み合わせたシステムを構築することで自動化できるとは思います。しかし、少なくとも今回個人で休日に小規模で検証する分には、そうした大掛かりなものを導入するのは選択肢として上がりませんでした。もし資金も時間も人手もあるようなビッグテックが似たようなことを大規模にやれば、ハイパーバイザのようなソフトウェアでも当然自動化は可能だと思います。

AI Slopと脆弱性報告

AIによって生成された低品質なコンテンツのことをAI Slopと言います。脆弱性報告においてもそのような検証がされていない報告がOSSに対して大量になされている現状があります。curlの作者がLLMで生成された低品質な脆弱性報告の対応に疲弊しているという記事を投稿していました。問題となっているのは、LLMを使って脆弱性を見つけること自体ではなく、得られた回答をレビュー検証せずに報告している点にあります。

また、今回特に印象的だったのは、「LLMができること」よりも「現状のLLMには心理的に任せられないこと」が浮き彫りになったことでした。特に報告先の開発者の方とのやりとりという面では、LLMにやりとりをさせるというのは絶対に選択肢としてあり得ないと思いました。今回は報告する前に指摘のされた脆弱性をちゃんと理解し実機で検証した上で報告を行いました。つまりLLMが出してきたレポートを十分に検証せずにそのまま開発者の方に送る、ということは決してできないということです。

LLMは計算リソースさえあればいくらでもスケールしますが、その報告に対応する開発者は人間であり、人間の時間は貴重という非対称性がある以上、「報告者には開発元に届く報告は可能な限り正確で高品質となるよう努力する義務がある」と今回開発者側から報告者側に回ったことで強く感じました。こうした検証を徹底する、というのは脆弱性報告の信頼とエコシステムを維持するためにも必要なことだと思います。

まとめと今後の展望

本記事では、普段開発者としてハイパーバイザ開発に関わっている人間がLLMの力を借りつつ脆弱性の調査と検証報告を行い攻撃者の視点に立ってみる、という個人的な取り組みを紹介しました。この活動を通して、LLMを利用した脆弱性診断において品質と信頼を確保するための知見が得られました。

今回の調査方針である「LLMをツールなしで活用する」という指針を与えてくれた記事の著者Sean Heelanさんが、先日新たな記事を投稿しました。今後の展望について重要な示唆を与える記事なので少し紹介します。

この記事ではQuickJSの0-dayを使ったexploitをLLMに開発させる、という一歩進んだ取り組みを行っています。結果はGPT-5.2が用意したすべてのシナリオでexploitを開発できたとのことでした。著者はこの結果からexploit開発などのoffensive securityの工業化に備えなければならないと結論づけています。

このSean Heelanさんの取り組みではLLMに試行錯誤させるためにデバッガなどのツールを提供していますが、今回私のやり残したこととして、このツールの提供というのが挙げられます。今後の展望としては、そうしたツールを提供したうえで、最新のGPT-5.2などのモデルを用いて検証のサイクルを自動で回すことで新たな知見が得られるかを調査したいと考えています。

個人プロジェクトから組織的な取り組みへ

私の休日プロジェクトと前後して、弊社では生成AIを活用した組織的な脆弱性検出プロジェクトを実施しました。2025年10月の1週間で13件の脆弱性を発見・報告しています。

今回の私の個人的な取り組みで得られた知見──「LLMに適切なコンテキストを与える」「人間による検証を徹底する」「責任ある開示を行う」──は、弊社の脆弱性診断サービスの強化にも寄与しています。

詳細はこちらのプレスリリースをご覧ください。

脆弱性診断をご検討中の方へ

弊社では、AIと人間の専門家を組み合わせた診断サービスを提供しています。お客様側でAI診断ツールを導入しなくてもAI活用のメリットを受けられます。

一緒に働きませんか?

弊社では、このような最先端の技術と実践的なセキュリティ研究を組み合わせた環境で働くメンバーを募集しています。開発者からセキュリティエンジニアへのキャリアチェンジも歓迎です。週次のpwn勉強会では、世界トップレベルのCTFプレイヤーから直接学べます。