セキュリティ
Avastアンチウイルスサンドボックスへの侵入と破壊 Part 2
Entering and Breaking the Avast Antivirus Sandbox Part 2 (safateam.com)
要約
本記事は、Avastアンチウイルス製品のカーネルドライバーに存在するCVE-2025-13032というダブルフェッチ脆弱性を悪用し、Windows 11システム上でSYSTEM権限を奪取するまでの詳細な技術的解説です。発見された脆弱性から、カーネルメモリの任意読み書きプリミティブの獲得、トークン窃盗による権限昇格に至るまでのステップが解説されています。
全文翻訳
すべての投稿
CVE-2025-13032: Avastアンチウイルスサンドボックスへの侵入と破壊 Part 2
この記事は、Avastアンチウイルスに関する調査の第二部であり、最終部です。最新のWindows 11システムにおけるCVE-2025-13032の完全なエクスプロイトについて詳述します。Part 1で紹介されたダブルフェッチ脆弱性から始まり、制御されたページプールオーバーフローが、IORingオブジェクトのRegBuffers配列を破損することによってどのようにカーネルの任意読み書きプリミティブに変換されたかを解説します。本記事では、ヒープスプレー戦略、MDLイントロスペクションによるカーネルアドレスリーク、クリーンアップ時のブルースクリーン回避に必要な修正、そして最終的なトークン窃盗によるSYSTEMへの権限昇格について説明します。
SAFA Team著
公開日: 2026年9月18日
共有
コピー!
はじめに
このブログ記事は、Avastに関する調査の第二部であり、最終部です。Avastのカーネルドライバーで発見したダブルフェッチ脆弱性であるCVE-2025-13032のエクスプロイトに焦点を当てます。
この記事では、バグを振り返り、発見当時、最新のWindows 11システムでどのように悪用したかを説明します。
もし見逃している場合は、パート1を自由に読んでください → https://www.safateam.com/intelligence-hub/research/technical-articles/cve-2025-13032-entering-and-breaking-the-avast-antivirus-sandbox-part-1
注意: 最新バージョンでは、Windowsカーネルとドライバーはユーザーモードアクセサー(https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/user-mode-accessors)を使用して、ユーザーモードメモリへの各カーネルアクセスを検証し、アクセスごとにユーザーバッファが実際にユーザー空間に存在することを確認しています。この緩和策は、この解説で説明されているエクスプロイト技術の使用を防ぎます。詳細については、https://www.youtube.com/watch?v=ry4SNYe2f68 を参照してください。
バグの説明
悪用したいバグは、カーネルプールオーバーフローにつながるダブルフェッチの問題です。
以下に示すコードスニペットは、ユーザーから提供された_UNICODE_STRING構造体をキャプチャすることを目的としていますが、ユーザー入力のLengthフィールドが複数回フェッチされるため、ダブルフェッチの問題が発生します。
最初のフェッチは、文字列がコピーされるバッファを割り当てるために行われ、2回目のフェッチは、取得された値に基づいてmemcpyを実行するために行われます。これにより、ユーザーがこれらのアクション間で値を変更した場合、プールオーバーフローが発生します。
1if ( !a1 && unicodestring_user )
2{
3 ProbeForRead(unicodestring_user, 0x10, 1); // unicodestring_userがユーザーランドにあるかチェック
4 ProbeForRead(unicodestring_user->Buffer, unicodestring_user->Length, 1); // バッファがユーザーランドにあるかチェック
5}
6v17 = sub_140071C6C((__int64)v18, &v14[v23 + 13], unicodestring_user); // unicodestring_userをキャプチャ
7[...]
8__int64 __fastcall sub_140071C6C(__int64 a1, _QWORD *a2, _UNICODE_STRING *unicodestring_user)
9{
10 PoolWithTag = ExAllocatePoolWithTag(PagedPool, unicodestring_user->Length + 16, 0x20786E53u); // 現在の長さに基づいてバッファを割り当て
11 if ( !PoolWithTag )
12 return 0xC000009A;
13 Length = unicodestring_user->Length;
14 *PoolWithTag = unicodestring_user->Length;
15 PoolWithTag[1] = Length;
16 *((_QWORD *)PoolWithTag + 1) = PoolWithTag + 8;
17 Buffer = (char *)unicodestring_user->Buffer;
18 if ( Buffer )
19 {
20 if ( unicodestring_user->Length )
21 memmove((_OWORD *)PoolWithTag + 1, Buffer, unicodestring_user->Length); // 新しくフェッチされた長さに基づいてデータをコピー
22 }
23 *a2 = PoolWithTag;
24 return 0;
25}
ダブルフェッチを悪用するために、セカンドスレッドがタイトなループで実行され、共有される_UNICODE_STRINGのLengthフィールドを小さな安全な値と大きな悪意のある値(例: 0x1000、割り当てられたバッファより大きい)の間で継続的に切り替えます。メインスレッドはループで脆弱なIOCTLを呼び出します。タイミングが合致すると、カーネルは割り当てのためにLengthを小さく読み込み、その後memmoveのために大きく読み込みます。これにより、割り当てられたよりも多くのバイトがコピーされ、プールオーバーフローが発生します。レースウィンドウは狭いですが、比較的少ないイテレーションで確実に勝つことができます。
私たちの目標は、このプールオーバーフローを悪用して、カーネルの任意読み書きプリミティブを獲得し、ローカル権限昇格を達成することです。このバグは、エクスプロイトに有利な条件を提供します。オーバーフローはPAGED_POOLを対象とし、割り当てサイズとオーバーフローサイズの両方が制御可能であり、コンテンツも同様です。
Paged Poolは、カーネルまたはドライバーが必要とするオブジェクトとデータに使用されるWindowsカーネルメモリの領域ですが、ディスクにページアウトされる可能性があります。これは、高優先度で実行されるクリティカルコードによってアクセスされる必要のないメモリに使用されます。アロケーターは割り当てをサイズクラスごとにグループ化するため、同じサイズのオブジェクトはメモリ内で互いに近接する傾向があります。これはヒープスプレーを可能にする特性です。Windows 10 19H1以降、これはセグメントヒープによって処理され、2つのバックエンドを使用します。小さい割り当てにはLFH(サイズバケット内でランダムに空きスロットを選択)、大きい割り当てにはVSアロケーター(適切なサイズの最初の利用可能なチャンクを提供)です。それぞれ異なるスプレー戦略が必要です。また、ほとんどのWindowsオブジェクトはページプールに格納されているため、破損させるオブジェクトを選択する際に多数の候補が存在することがわかります。次のセクションで、選択したオブジェクトとその理由を説明します。
Windowsプールがどのように機能するかについての詳細は、Synacktivの "Scoop the Windows 10 pool!" ペーパー(https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf)を参照してください。
I/O Ring Object
I/O Ring Objectは、非同期に実行されるI/O操作のサブミッションキューを維持するオブジェクトです。
具体的には、ユーザーランドがファイルI/Oリクエストをバッチ処理できるようにします。IoRingReadFileはファイルをプリレジスタされたバッファにコピーし、IoRingWriteFileはプリレジスタされたバッファからファイルにデータをコピーします。これらのレジスタされたバッファ(_IORING_OBJECTのRegBuffersフィールドで追跡)は、登録時に一度検証され、その後すべての後続操作で自由に再利用されるため、破損の対象として永続的で興味深いものになります。このオブジェクトを破損ターゲットとして選択した理由はいくつかあります。IORingオブジェクト自体はNON_PAGED_POOLに配置されていますが、そのRegBuffersフィールドはPAGED_POOLに割り当てられており、オーバーフローが発生するプールと直接一致します。第二に、RegBuffers割り当てのサイズは完全にユーザー制御可能です。N個のバッファを登録すると、各8バイトのポインタの配列が生成され、割り当てサイズを正確に制御でき、ヒープスプレーに最適です。第三に、その配列内の単一のポインタを破損するだけで、完全な任意読み書きプリミティブを獲得できます。より複雑な構造を破損する必要はありません。最後に、I/O Ring Objectsはすでにこの目標を達成するために公開で使用されており、この手法を確認し、アプローチの確固たる参照点を提供します。(https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/)
ユーザーランドからオブジェクトを使用できるAPIは複数あります。以下はその一部です。
- CreateIoRing
- CloseIoRing
- BuildIoRingReadFile
- BuildIoRingWriteFile
- BuildIoRingRegisterBuffers
- BuildIoRingRegisterFileHandles
- SubmitIoRing
- ...
Build.* APIは、SubmitIoRing APIを通じて送信する必要のあるエントリを構築するために使用されます。
IoRingRegisterBuffersは、ユーザーが将来のI/O Ring操作のためにバッファの配列を登録できるようにします。これは、IoRingReadFile操作の宛先バッファとして、またはIoRingWriteFileのソースバッファとして使用できます。このアクションは、_IORING_OBJECT内のRegBuffersポインタ配列を作成し、それが指す個々の_IOP_MC_BUFFER_ENTRYオブジェクトを割り当てます。各オブジェクトは、登録されたバッファに関する情報を含んでいます。
_IORING_OBJECTと_IOP_MC_BUFFER_ENTRY構造体を以下に示します。
1struct _IOP_MC_BUFFER_ENTRY
2{
3 unsigned __int16 Type;
4 unsigned __int16 Reserved;
5 unsigned int Size;
6 int ReferenceCount;
7 _IOP_MC_BUFFER_EN