
はじめに
Armory Crate と AI Suite は、ASUS マザーボードや、プロセッサ、RAM、人気の高まっている RGB 照明などの関連コンポーネントを管理、監視するためのアプリケーションです。こうしたアプリケーションは、一般にドライバをシステムにインストールします。これらのドライバは、設定を行ったり、CPU 温度、ファン速度、ファームウェア更新などの重要なパラメータを取得したりする際に、ハードウェアと直接通信するために必要となるものです。
そのため、ドライバがセキュリティを考慮して適切に作成され、ドライバインターフェイスへのアクセスが特定のサービスと管理者のみに制限されるように設計されていることが重要です。

図 1. Armory Crate アプリケーション
Cisco Talos が上記アプリケーションに関連するコードとコンポーネントを監査したところ、AsIO3.sys ドライバに次の 2 つの重大な脆弱性があることを発見しました。どちらの脆弱性も IRP_MJ_CREATE ハンドラ内にありました。
- CVE-2025-1533/TALOS-2025-2144 – Asus Armoury Crate AsIO3.sys スタックベースのバッファオーバーフローの脆弱性

- CVE-2025-3464/TALOS-2025-2150 – Asus Armoury Crate AsIO3.sys 承認バイパスの脆弱性

第 1 の脆弱性は、プロセスが ImagePath を「Win32 のパス」から「NT 名前空間のパス」に変換するときに発生するスタックベースのバッファオーバーフローです。
第 2 の脆弱性は、ドライバに実装された承認メカニズムをバイパスできるというもので、意図したサービスだけでなく任意のユーザーが機能にアクセスできてしまいます。このドライバ内にあるセキュリティ上重要な機能にアクセスすることによって、ローカルユーザーの権限を「NT SYSTEM」に昇格させるエクスプロイトの開発に成功しました。ここでは、このエクスプロイトについて詳しく説明します。
なお、このエクスプロイトは、Windows 11 24H2 アップデートがリリースされる前に作成しました。このアップデートは、「NtQuerySystemInformation」経由でロードされたカーネルモジュールやそのアドレスなどの情報を一般ユーザーが漏洩するのを防ぎます。この点については後で詳しく説明します。
調査フェーズ
Armory Crate ソフトウェアとともにインストールされたドライバを探していたところ、DriverView リストに ASUS 関連の 2 つのドライバがあることに気付きました。

図 2. ASUSTek Computer Inc. に属すドライバエントリを表示するスクリーンショット
まず、AsIO3.sys に着目し、このドライバがデバイスを作成するかどうか、そして最も重要な点として、一般ユーザーがそのデバイスと通信できるかどうかを調査しました。
Asusgio3 へのハンドルの取得
下の画像にあるとおり、DeviceTree を使用すると、エクスプロイトの参考になりそうな情報を確認できます。

図 3. DeviceTree は、グループ「Everyone」が Asusgio3 デバイスへのフルアクセス権を持っていることを誤って表示
AsIO3.sys ドライバは Asusgio3 デバイスを作成します。このデバイスには、システム内のほぼすべてのユーザーがフルアクセスできます。このデバイスへのハンドルを開く簡単なコードで手短に確認したところ、アクセス拒否を示すエラーコード「5 == ERROR_ACCESS_DENIED」が表示されました。

図 4. Asusgio3 デバイスへのハンドルを開く役目を持つ PoC コードの一部
DeviceTree の表示内容からは予期しない結果だったので、Sysinternals の「accesschk」を使用してそのデバイスに対する権限を再確認したところ、まったく逆の結果でした。

図 5. 「accesschk」で Asusgio3 に対する権限を確認
どちらが本当なのかを明らかにするため、「IRP_MJ_CREATE」要求を処理する AsIO3 ドライバ内のフラグメントについてリバースプロセスを行いました。
「IRP_MJ_CREATE」ハンドラの分析
ドライバをロードしてリバースプロセスを開始すると、ある 1 つの関数が 3 つのタイプの IRP 要求を処理していることがわかりました。

図 6. IRP 要求ハンドラが DriverObject に割り当てられるドライバ初期化ルーチン
「callback_irp_dispatch」を詳しく調べてみると、「IRP CREATE」要求を処理するフラグメントが見つかりました。

図 7. 「callback_irp_dispatch」関数コードの一部(関数名は執筆者が追加)
承認メカニズム
「ImageHashCheck」関数を確認すると、次のような処理内容でした。

図 8. ImageHashCheck 関数の本体
ASUS 社の開発者は、API「ZwQueryInformationProcess」とフラグ「ProcessImageFileNameWin32」を組み合わせて(8 行目と 19 行目)、現在のプロセス(具体的にはデバイスへのハンドルの取得を試みるプロセス)の ImagePath(実行ファイルのパス)を取得しようとしています。
続く 26 行目では、「Win32 ファイルの名前空間」から「NT の名前空間」へのパスの変換が行われています。この行については後で説明します。
35 ~ 46 行目では、現在のプロセスの実行ファイルに対して典型的な SHA256 ハッシュ計算が行われています。47 行目では、計算後のハッシュとドライバ内のハードコードハッシュが比較され、一致する場合、関数は「true」を返し、デバイスへのハンドルの取得をプロセスに許可します。
グローバル変数「g_sha256Hash」(47 行目)からハッシュをダンプすると、次のようになります。
python Python>binascii.b2a_hex(idc.get_bytes(0x0000000140009150,32)) B'c5c176fc0cbf4cc4e37c84b6237392b8bea58dbccf5fbbc902819dfc72ca9efa'
「AsusCertService.exe」の SHA256 ハッシュを計算したところ、同じハッシュであることがわかりました。
powershell PS C:\Users\icewall> Get-FileHash -Path "C:\Program Files (x86)\ASUS\AsusCertService\AsusCertService.exe" -Algorithm SHA256 Algorithm Hash Path --------- ---- ---- SHA256 C5C176FC0CBF4CC4E37C84B6237392B8BEA58DBCCF5FBBC902819DFC72CA9EFA C:\Program Files (x86)\ASUS\AsusCertService\AsusCertService.exe
新たにわかった点を踏まえると、「AsusCertService.exe」サービスと、このサービスによって許可リストに追加された PID を持つプロセスのみが、Asusgio3 デバイスのハンドルを取得できます。それ以外の場合、この操作は「Access is denied」というステータスを返します。
Win32PathToNtPath – スタックベースのバッファオーバーフロー
「Win32PathToNtPath」関数で見つかった脆弱性については、この記事では詳しく説明しません。この脆弱性がエクスプロイトの後の段階で利用されることはないためです。しかし興味深い情報を付け加えておきます。
開発者は、Windows のパスの最大長をほぼ「MAX_PATH」の文字数である 260 文字と想定し、この想定に基づいて、受信した ImagePath の実際の長さを先に確認することなく、スタック上にある固定長(255 文字)のバッファにコピーしていました。しかしこの想定は誤りで、パスは約 260 文字を超える可能性があります。Microsoft 社のこちらのドキュメントには、「最大パスの 32,767 文字は概算値です。”\\?\” プレフィックスは実行時にシステムによって長い文字列に拡張される可能性があり、この拡張は合計長に適用されるためです」と記載されています。
この脆弱性の詳細については、アドバイザリ『CVE-2025-1533/TALOS-2025-2144 – Asus Armoury Crate AsIO3.sys スタックベースのバッファオーバーフローの脆弱性
』を参照してください。
承認バイパス
承認メカニズムが、「ZwQueryInformationProcess」API によって返される「ImagePath」と、このパスにある実行ファイルに対して計算された SHA256 ハッシュに基づいていることがわかったため、これをバイパスする手段を検討することにしました。
「Windows Research Kernel(WRK)」の「(Nt/Zw)QueryInformationProcess」の実装を調査した結果、現在のプロセスの「ImagePath」に関する情報は「EPROCESS」構造体から取得されることがわかりました。したがって、ユーザーモードからその値を操作することは不可能です。ただし、潜在的なバイパスの手段はまだ残っています。
ハードリンクによる抜け道
ハードリンクを使用すると、「ImageHashCheck」ルーチンをバイパスできます。まず、PoC.exe ファイルへのハードリンクを作成します。

図 9. 「PoC.exe」を指すハードリンクの作成
「PoC.exe」には今のところ大きな機能はありません。ユーザー入力を待ってから、Asusgio3 デバイスへのハンドルを開こうとするだけです。

図 10. Asusgio3 デバイスへのハンドルを開く役目を持つ PoC.exe ファイルの一部
「PoC.exe」を直接実行する代わりに「run.exe」というハードリンクを実行すると、EPROCESS 構造体の ImagePath がハードリンクを指すようになります。
run.exe(PoC.exe)を実行し、ユーザー入力を待つ間にこのハードリンクを削除し、AsusCertService.exe を指す同じ名前の新しいハードリンクを作成します。しかし元の AsusCertService.exe の場所を指す直接のハードリンクを作成しようとすると、次のようにアクセス拒否が表示されます。
![]()
図 11. 緩和策の実装により、AsusCertService.exe の直接の場所を指すハードリンクを作成しようとしても失敗
Microsoft 社が数年前に導入した緩和策により、ユーザーは上書き権限を持つファイルにしかハードリンクを作成できません。しかし今回のケースでは、ファイルを一時的な場所にコピーしてからハードリンクを作成すればよいため、これは問題にはなりません。

図 12. AsusCertService.exe のローカルコピーを指すハードリンクの作成に成功
これで、先ほど実行した PoC.exe プロセスをさらに進めることができます。このシナリオでは、PoC.exe が Asusgio3 デバイスへのハンドルを開こうとすると、run.exe のハードリンクが AsusCertService.exe ファイルを指し、SHA256 ハッシュが一致します。その結果、承認メカニズムをバイパスすることができます。
強力なエクスプロイトプリミティブの発見
ドライバの機能の分析
AsIO3.sys ドライバの IOCTL ハンドラのコードを調べていたところ、エクスプロイト開発に適したプリミティブとなる以下の機能を発見しました。一般ユーザーとして、適切な IOCTL で以下の操作などを実行できました。
- モデル固有のレジスタ(MSR)の読み取り/書き込み
- 任意の物理メモリ(アドレス、サイズ)をプロセスの仮想メモリにマッピング
- I/O ポートの読み取り/書き込み
しかし、このエクスプロイトは当初予想したよりも困難であるとわかりました。
MSR の変更によるエクスプロイトの試み
セキュリティの観点から重要な MSR レジスタが少なくとも 2 つあります。
- IA32_LSTAR(0xC0000082)
- IA32_SYSENTER_EIP(0x00000176)
これらの MSR レジスタは、SYSCALL または SYSENTER 命令が呼び出されたときに実行がリダイレクトされるカーネル内のアドレスを定義します。これらのレジスタを変更すると、制御フローを乗っ取り、特権アクセスで任意のコードを実行できる可能性があるため、カーネルエクスプロイトにおける重要な手段となります。ここで、有望と思われる IOCTL 0xA040A45C 用のハンドラが見つかりました。このハンドラでは、MSR レジスタを任意のデータで上書きすることができます。

図 13. MSR レジスタ変更のための数少ない方法を提供する IOCTL ハンドラ
16 行目の「_writemsr」命令では、「SystemBuffer」(8 行目)から取得した制御可能なデータが MSR レジスタのインデックス(「msrReg」)および値(「msrRegVal」)として使用されます。
一見これは有望に見えますが、11 行目に「msrReg」の値(インデックス)をチェックする呼び出しがあります。詳しく見てみましょう。

図 14. 限られた MSR レジスタに対してのみ変更を許可する MSR のフィルタリング関数
MSR インデックスは、「MSR_allowedList」配列にある許可された MSR インデックスのリストと照合されます。あいにく、このリストには、前述の重要なレジスタ、「IA32_LSTAR(0xC0000082)」と「IA32_SYSENTER_EIP(0x00000176)」は含まれていません。そこで、インデックスをデコードして、レジスタ名とその用途を確認したところ、操作できるのはセキュリティに影響を与えないレジスタのみであることがわかりました。

図 15. 許可された MSR インデックスの一部
以上のことがわかったので、代わりのエクスプロイト方法を探す必要がありました。
物理メモリマッピング
エクスプロイトプロセスに役立ちそうな他のコードを探していたところ、物理メモリをプロセスの仮想アドレス空間にマッピングできそうな IOCTL ハンドラをいくつか見つけました。その 1 つが「0xA040200C」です。

図 16. 物理メモリを呼び出し側の仮想メモリにマッピングする関数の本体
この関数に渡される引数「phyAddress」、「memSize」の値は完全に制御できます。一見したところ、任意の物理メモリをユーザー空間にマッピングできるようです。このプリミティブはいくつかの方法で活用できます。その一部を示します。
- 変更したい重要なカーネルデータの仮想アドレスを物理アドレスに変換し、上記のコードを使用してユーザー空間にマッピングします。このアドレス変換はユーザーモードではできないため、カーネルレベル API「MmGetPhysicalAddress」を使用する必要があります。
- 物理メモリの連続した領域を順にマッピングし、SYSTEM プロセスの EPROCESS 構造体などの構造体を検索して、後でプロセスのセキュリティトークンを SYSTEM プロセスに属すトークンに置き換えます。
- 「Low Stub」(PPROCESSOR_START_BLOCK 構造体)に関する知識を使用して CR3 レジスタ(PML4 ベースアドレス)の値を読み取ってから、メモリページング構造体から他のエントリを読み取って仮想アドレスを物理アドレスに手動で変換します。
Russell Sanford 氏のプレゼンテーション『Exploiting LOLDrivers』
ではこれらの方法について詳しく説明していますが、今回は状況に合った方法を選択する必要がありました。
あいにく、このドライバでは MmGetPhysicalAddress API を直接呼び出す方法がないため、仮想アドレスを物理アドレスに直接変換することはできません。物理メモリの検索は非常に時間がかかり、問題が発生する可能性もあります(他の実装例や、この方法を選択した際に発生する問題についての報告を参照してください)。
そこで最終的に、「Low Stub」方式を実装して仮想アドレスを物理アドレスに手動で変換することにしました。その前に、18 行目で呼び出されている「checkPhyMemoryRange」という関数に着目しました。

図 17. 物理メモリの特定の範囲のみをマッピング可能にする checkPhyMemoryRange の本体
開発者は「g_goodRanges」変数に特定の物理メモリアドレス範囲を定義していました。指定された範囲が定義済みの範囲に一致しない場合、この関数は true を返し、実行を継続してエラーコードを返します。
「Low Stub」、つまり「PPROCESSOR_START_BLOCK 構造体」の位置を確認してみると、それを読み取ることができます。同様に、PML4 ベースアドレスを指す CR3 レジスタの値も読み取ることができました。
メモリマッピング構造体の次のエントリは、許可されたアドレス範囲外の場所を指していました。そのため、このアプローチは断念しました。
1 を引くだけですべてを支配
役に立ちそうな新しいコードを探していたところ、次の「IOCTL 0xa0402450」ハンドラを見つけました。

図 18. ユーザーが制御する任意のアドレスで「ObfDereferenceObject」を呼び出すことを許可する IOCTL ハンドラ
ユーザーは 3 つの引数をすべて完全に制御できます。一見するとこのコードはまったく無害に見えるかもしれませんが、「ObfDereferenceObject」の内部を詳しく調べたところ、次のようになっていました。

図 19. 「ObfDereferenceObject」API の実装の一部
「ObfDereferenceObject」に任意のアドレスを渡すことができるため、任意のメモリ値から 1 を引くことができます。正確に言うと、「ObfDereferenceObject」を使用して「Object – 0x30」にあるメモリ値から 1 を引きます。この点を念頭に置いてエクスプロイトを作成しました。
パズルのピースは揃ったか
しかしこれらのプリミティブをどのように役立てればよいでしょうか。メモリリークを追加する必要はあるでしょうか。完全に動作するエクスプロイトを作成しようと決めたとき、コードがローカルユーザーによって実行されるシナリオを想定しました(プロセス完全性レベル:中)。Windows のエクスプロイトプロセスに詳しい方は、NtQuerySystemInformation がカーネル構造体に関する非常に有益な情報を提供できることをご存知かと思います。
しかし 2025 年の現在、Windows 11 が使用されています。ここで、「NtQuerySystemInformation」経由でロードされたカーネルモジュールやそのアドレスなどの情報を一般ユーザーが漏洩するのを防ぐ緩和策が間もなくリリースされるというニュース
を思い出しました。
完全に動作するエクスプロイトを作成した 2 月初旬の時点では、私が使用している Windows 11 にはまだ 24H2 アップデートが適用されておらず、「ntoskrnl.exe – 10.0.22621.4890(WinBuild.160101.0800)」のままでした。
2025 年 3 月にこの記事を書き終えた頃、ついに 24H2 アップデート(「ntoskrnl.exe -10.0.26100.3476(WinBuild.160101.0800)」)が届き、「NtQuerySystemInformation」でカーネルアドレスを漏洩することは不可能になりました。

図 20. 上半分は緩和策適用前の ntoskrnl.exe、下半分は適用後の ntoskrnl.exe
エクスプロイトの作成
以上の知識を踏まえ、エクスプロイトの作成を開始しました。
自身のスレッド KTHREAD 構造体のアドレスをリーク
前の段落で述べたように、ユーザーは「NtQuerySystemInformation」API を利用して、自身のスレッドを示す「KTHREAD」構造体のアドレスなどをリークできます。ここで、「1 を引く」という単純なプリミティブが役立ちます。
オフセット「0x232」の「KTHREAD」構造体には「PreviousMode」というフィールドがあり、ユーザーモードのスレッドの場合は 1 に設定されます。このフィールドは非常に重要であり、複数のカーネルレベル API によって値がチェックされ、ユーザーがユーザーモードから特定の syscall を呼び出すと、最終的に機能が制限されます。
たとえば、API が「ReadProcessMemory」を呼び出したときにどうなるか見てみると、「ReadProcessMemory」は syscall「NtReadVirtualMemory(MiReadWriteVirtualMemory)」を呼び出します。

図 21. NtReadVirtualMemory の実装のうち、PreviousMode フィールドの意味を示す部分
冒頭を見るとわかるように、この syscall は 11 行目で現在のスレッド構造体を取得します。続いて 13 行目には、PreviousMode が 1(ユーザーモード)に設定されている場合の特別な条件が設定されています。23 行目では、ユーザーが指しているアドレス(「BaseAddress」)と要求されたメモリサイズを合計した値が、ユーザーモード コンポーネントがマッピングされている最大アドレスを超えていないかどうかを検証しています。これにより、ユーザーモードから呼び出しを行うユーザーは、カーネルモードアドレス空間からメモリを読み取ることができなくなります。
この事実に基づき、自分のスレッドの PreviousMode を「1」から「0」に減らすことにより、スレッドの状態をユーザーモードからカーネルモードに事実上変更しました。これで、アドレス空間全体を読み書きできるようになります。
自身のスレッドを示す「KTHREAD」のアドレスを見つけるために、次の手順を実行しました。

図 22. 「NtQuerySystemInformation」を使用して、システム内で開いているすべてのハンドルに関する情報を取得
まず、自身のスレッドを識別するために、8 行目でスレッドのハンドルを開きました(後でこのハンドルの値を使用して、スレッドの構造体に関する情報を特定しました)。次に「SystemHandleInformation」クラスを使用して「NtQuerySystemInformation」を呼び出し、システム内のすべてのハンドルに関する情報を取得しました。その後で自身のスレッドハンドルを特定するために、ハンドル値、プロセス ID、オブジェクトタイプ(スレッド)で結果を絞り込みました。

図 23. 自身のスレッドに関連する構造体を検索
PreviousMode の変更
「KTHREAD」のアドレスと、「PreviousMode」フィールドを変更するためのプリミティブが得られたので、それらを組み合わせました。

図 24. エクスプロイトコードの最初の部分
EPROCESS 構造体へのポインタは、KTHREAD にある EPROCESS 構造体の位置情報を使用するだけで取得できました。EPROCESS については後述します。すでに説明したように、「ObfDereferenceObject」は、引数として渡されたアドレスから 0x30 を引いたものです。そのため、900 行目では PreviousMode アドレスに 0x30 を加えます。
次に、903 行目の一時停止処理により、シンボリックリンクのリンク先を入れ替え、Asusgio3 デバイスへのハンドルを開く前に承認メカニズムをバイパスする時間があります。「DecrementPreviousMode」関数内では、「Asusgio3」へのハンドルを開き、適切にフォーマットされたバッファを送信してプリミティブをトリガーしました。

図 25. IRP 要求を送信して任意のアドレスで「ObfDereferenceObject」API への呼び出しをトリガーするコード
トークンの窃取
これで、スレッドの「PreviousMode」フィールドが「カーネルモード」に変更されたため、仮想アドレス空間全体を読み取れるようになりました。こうして最初に行ったステップは、EPROCESS 構造体のアドレス位置を読み取って保存することです。

図 26. EPROCESS 構造体のアドレス位置の読み取り
自身の EPROCESS 構造体のアドレスを入手したので、プロセスのリンクリストから SYSTEM プロセス(PID == 4)の検索を開始しました。そのために使用したのが、EPROCESS 構造体の中にある「ActiveProcessLinks」というフィールドです。これは、システム内にあるすべてのプロセスの双方向リンクリストです。

図 27. プロセスのリンクリストを走査して、SYSTEM プロセスを検索
SYSTEM プロセスに属す EPROCESS 構造体が見つかると、そのセキュリティトークンを読み取り、先ほど読み取ったトークンに置き換えることができます。このとき、SYSTEM トークンの参照カウントを忘れずに足すようにします。

図 28. ユーザー自身のセキュリティトークンを SYSTEM のセキュリティトークンに置換
昇格された権限でのコンソールの実行
これで、昇格された権限でコンソールを実行できるようになりました。

図 29. コンソールの実行(SYSTEM レベルに昇格された権限で実行することが必要)
次のように表示されます。

図 30. 完全に機能するエクスプロイトの実行とその結果
成功です。このエクスプロイトにより、コンソールの権限は SYSTEM レベルに昇格されます。動作の様子は、こちらの動画
をご覧ください。
まとめ
リバースプロセスを進める中で、開発者が特定のドライバ機能を制限することで、報告済みの脆弱性やエクスプロイトプリミティブを修正していることに気付きました。しかし、禁止リストによるアプローチは、明示的にブロックされていない機能を 1 つ見つけるだけでエクスプロイトできるため、決して良いセキュリティ対策とは言えません。代わりに、許可リストを実装し、必要な機能のみに制限する方が効果的です。
さらに重要なのは、このような重要かつ潜在的に危険な機能を備えたドライバは、複数のセキュリティ層で厳密にアクセスを制御し、権限を持つ限られた数のシステムユーザーのみに利用を許可する必要があるという点です。
最後に、このリサーチで実証されたのは、「1 を引く」といった一見単純なプリミティブさえ利用すれば、完全に機能する権限昇格エクスプロイトを開発できることです。これにより、カーネルモード コンポーネントでは慎重なセキュリティ設計が重要であると明らかになりました。
本稿は 2025 年 6 月 26 日にTalos Group
のブログに投稿された「Decrement by one to rule them all: AsIO3.sys driver exploitation
」の抄訳です。