



1. はじめに
こんにちは。私たちはENKI WhiteHatのセキュリティ研究者であり、Windows Bug Hunterであるキム・ジョンソン、キム・ドンジュンです。私たちは2024年から2025年最近まで、Windowsでの権限昇格脆弱性を発見するための研究を行ってきました。
私たちは、WindowsでSYSTEM権限およびUser権限で動作するサービスと、Windows AppContainer権限から呼び出すことができるアタックサーフェスに関する研究を行い、最終的に合計10件以上の脆弱性を報告しました。
このブログポストでは、私たちの「COM-pletely Unplanned(完全に想定外)」な旅路に沿って、どのようなコンポーネントを選択し、どのようにアプローチしたのか、そしてその過程でどのような興味深い脆弱性を発見することになったのかをお話ししたいと思います。
私たちの研究結果は、2025年5月8日から9日までシンガポールのセキュリティ企業StartLabsが主催したOff-By-One 2025カンファレンスでも発表されました。
1.1 Why Local Privilege Escalation Still Matters
Windowsは世界中で最も広く使用されているオペレーティングシステムの一つであり、それだけに攻撃者やセキュリティ研究者の主な関心の対象となってきました。膨大なコードベース、複雑なアーキテクチャ、そして多様な権限モデルを持つWindowsは、様々なタイプの脆弱性を引き起こす可能性のある構造を持っています。
この中でも、隔離されたサンドボックス環境から一般ユーザー権限まで、一般ユーザー権限からシステム権限まで到達できる権限昇格(Local Privilege Escalation, LPE)脆弱性は、長年にわたり優先度の高い研究テーマ研究テーマでした。
現代の多くのソフトウェアは、それぞれの方法でサンドボックス化された形態のアーキテクチャを実装し、運用しています。これは、単に一つの脆弱性で終わるのではなく、Sandbox Escape、バックドアの設置など、複数の攻撃シナリオの基盤となるためです。このため、現代のExploit chainは、一つのバグだけが使用されるのではなく、n個のバグのチェーン化が必須となっています。

1.2 Alternative Attack Vectors
実際に多くの権限昇格脆弱性はWindowsカーネルに集中しており、代表的な例としてwin32k, ALPC, ntoskrnl, Windows Device Driverなどが繰り返し分析の対象となってきました。これらのコンポーネントはユーザーとカーネルの境界線上に位置し、攻撃者が比較的直接アクセスできるため、長年にわたり権限昇格攻撃の核心的なベクトルと見なされてきました。
これに対抗して、Microsoftは数多くの脆弱性パッチとSecurity Mitigation(セキュリティ緩和策)を適用しており、攻撃者やセキュリティ研究者が脆弱性を見つけることは徐々に難しくなっています。

https://cloud.google.com/blog/topics/threat-intelligence/2024-zero-day-trends
このような流れの中で、従来とは異なる権限昇格の経路を探そうとする試みが自然と登場しています。私たちも同様に、伝統的なカーネル攻撃ベクトル以外の経路で権限昇格が可能な構造が残っているのではないかという疑問から、研究を開始することになりました。
その過程で私たちが注目したのは、低い権限からアクセスできるものの、内部的には呼び出し元よりも高い権限で動作するコンポーネントでした。特に以下のような特性を持つ対象を、主な探索候補として選定しました:
SYSTEM権限で実行されるサービス
一般ユーザー(Normal Integrity User)やLow Privileged AppContainerのような制限された権限からでもアクセス可能なサービス
これらの条件をベースに、Windowsのアーキテクチャを分析していたところ、ある興味深い構造が目に留まりました。それがCOM (Component Object Model)でした。
実は、最初からCOMをターゲットにしようとしていたわけではありませんでした。COMで構成されたWindows Runtimeをサポートする一部のサービスと、その他のLocalサービスが、低い権限(User、AppContainerなど)から高い権限を持つプロセスへ直接リクエストを送信できることを発見し、この小さな手がかりが私たちの研究の方向性を決定づけました。
そうして、まったく計画にはありませんでしたが、素晴らしい成果を上げることができた「COM-pletely Unplanned」な旅が始まりました。

2. COM
私たちは普段使用しているWindows機能の大半が、COMというもので実装されています。Windows Explorer、Microsoft Office、Windows Firewall、Speech APIなどもすべてCOMを基盤に動作しています。では、一体このCOMとは何なのでしょうか?

2.1 Basic Concepts of COM
まず、COMの脆弱性分析を開始する前に、COMが何であるかを正確に理解する必要がありました。一緒に簡単に紐解いてみましょう。
COMはMicrosoftが設計したコンポーネントベースのソフトウェアアーキテクチャで、異なるプロセスやモジュール間でオブジェクトを作成し、データを送受信できるようにします。特にCOMの最大の強みの1つは、さまざまなプログラミング言語でCOMオブジェクトを作成できる点です。COMは、C/C++、C#、PowerShell、Pythonなどの多様な言語から同じ方法でアクセスできるインターフェースを提供します。
例として、以下のようにPowerShellとPythonを使用し、言語に依存しない共通の方法でExcel(およびその他のWindowsコンポーネント)にデータを入力することができます。

しかし、これはどのようにして可能になっているのでしょうか?
COMはオブジェクトを識別するために、CLSID (Class ID)とIID (Interface ID)を使用します。CLSIDはCOMクラスの一意の識別子であり、レジストリに登録され、該当するオブジェクトを作成できる場所や方法を定義します。IIDは、そのCOMクラスが提供する各インターフェースの一意の識別子です。これにより、クライアントは特定のCLSIDを使用してオブジェクトを作成し、そのオブジェクトが提供するインターフェースをIIDを介して使用できるようになり、他の言語でCOMオブジェクトを呼び出しても、同じバイナリインターフェースを共有します。

2.2 COM Server Models
では、先ほどの例のようにCOMを介してExcelの機能を使用できるとすれば、このCOMオブジェクトが実際にどこで実行され、どのような権限で動作するかはどのように決定されるのでしょうか? COMサーバーは、実行方式によって大きく以下の3つに分かれます。

In-Process Server (InProcServer) 方式は、COMサーバーがDLL形式で実装されています。この方式でCOMオブジェクトを使用すると、クライアントと同じプロセス内で実行されるため、クライアントと同じ権限を持つことになります。簡単に言えば、自分のプロセスがCOMサーバーの実装されているDLLをロードして使用するようなものです。
Out-of-Process Server (LocalServer) 方式は、COMサーバーがEXE形式で実装されています。この方式は、クライアントとは別個のプロセスで実行されるため、COMサーバープロセスのセキュリティ設定によって権限が異なります。spoolsv.exeやmsiserver.exeのように、固有の名前を持つプロセス内にCOMサーバーが実装されている場合もありますが、多くはWindowsでサービスホスティングを担当するsvchost.exeが、COMサーバーの実装されたDLLをロードして使用します。
この方式は、クライアントが別のプロセスにリクエストを送信するため、Out-of-Process ServerではProxyとStubの構造を使用します。この構造については後ほど説明します。
Remote Server 方式は、クライアントアプリケーションとは異なるコンピュータで実行され、DCOM(Distributed COM)を利用してネットワークを介したリモート呼び出しをサポートします。本稿では詳しく扱いません。
2.3 COM Launch/Access Permission
しかし、ここで少し考えてみましょう。Out-of-Process ServerやRemote Serverを何の検証もなくすべてのプロセスから呼び出せてしまったら、どうなるでしょうか?
もし低い権限のユーザーでも、SYSTEM権限で動作するCOMサーバーを自由に実行し、インターフェースが提供するメソッドを呼び出せるようになれば、重大な権限昇格やリモートコード実行につながる可能性があります。これを防ぐために、COMはLaunch(起動)権限とAccess(アクセス)権限という2つの重要なセキュリティチェックを行います。
2.3.1 Launch Permission
Launch Permission(起動権限)は、クライアントアプリケーションがそのCOMサーバーを新しく起動(アクティベーション)できるかどうかを判断します。
例として、あるユーザーが特定のCOMクラスに対するLaunch Permissionを持っている場合、CoCreateInstanceなどの関数を介して、そのCOMサーバーを新しく実行することができます。
では、Access Permissionとの違いは何でしょうか?
2.3.2 Access Permission
Access Permission(アクセス権限)は、すでに実行中のCOMサーバーに対して、クライアントがそのCOMオブジェクトのメソッドを呼び出せるかどうかを判断します。この権限を持っている場合、すでに実行されているCOMサーバーにアクセスして、オブジェクトが公開しているインターフェースのメソッド呼び出し権限を得ることになります。
逆に、Launch PermissionがあってもAccess Permissionがない場合、COMサーバーは起動されますが、オブジェクトにアクセスしてメソッドを呼び出すことはできません。

2.4 COM Data Transfer
さて、ここまでCOMのサーバーモデルとセキュリティモデルについて簡単に見てきました。今回は、COMがどのようにデータを送受信しているのかについて見ていきましょう。
COMは前述したIn-Process Server方式を使用することで、単一プロセス内でもオブジェクト間通信を可能にしますが、Out-of-Process ServerやRemote Serverの場合は、プロセスやシステムが互いに異なるため、データ転送のための追加プロセスが必要です。まさにこの時に使用されるのが、Proxy/Stubとマーシャリング/アンマーシャリングです。

2.4.1 Proxy/Stub
ProxyとStubは、リモートでCOMオブジェクトの呼び出しを可能にする構造です。Proxyはクライアント側で代理人の役割を果たし、Stubはサーバー側で受信者の役割を果たします。たとえば、クライアントが別のプロセスにあるCOMオブジェクトのメソッドを呼び出すとき、リモートオブジェクトを直接呼び出すことはできないため、代わりにクライアントプロセス内に存在するProxyオブジェクトを呼び出すことになります。このProxyは、メソッド呼び出しに必要なデータをマーシャリングしてリモートに送信し、サーバー側のStubがこれをアンマーシャリングして、実際のオブジェクトのメソッドを呼び出します。サーバー側の戻り値も、逆の経路をたどってクライアントに返されます。では、マーシャリングとアンマーシャリングとは何でしょうか?

2.4.2 Marshalling/Unmarshalling
マーシャリングとは、プロセスのメモリ空間に存在するオブジェクトデータを、他のプロセスでも使用できるようにシリアル化するプロセスです。COMでは、このプロセスを介して呼び出したいインターフェースと引数をIDL(Interface Definition Language)で定義されたルールに沿って変換し、これをRPCベースで送信します。
一般的に、シリアル化はオブジェクトのデータを単なるバイトストリームに変換するプロセスと理解されますが、COMにおけるマーシャリングははるかに広い概念です。COMのマーシャリングは基本的に、メソッドの基本的な呼び出し引数(int、BSTRなど)を含め、IUnknown *、IInspectable *のようなインターフェースポインタなどを1つの呼び出しコンテキストとしてまとめて送信します。
逆にアンマーシャリングは、このようにマーシャリングされたデータをもとのデータに変換し、サーバー側で実際のオブジェクトデータとして使用できるようにするプロセスです。繰り返しになりますが、アンマーシャリングされたデータは、マーシャリング前のデータと同一の構造でなければならず、このためにマーシャリングとアンマーシャリングの双方が、IDLに定義されたデータ形式と順序に厳格に従う必要があります。もしこのルールに従わない場合、RPCランタイムで例外が発生したり、データの解釈に失敗したりします。

2.5 COM Threading Model
最後に、COMのThreading Modelについて簡単に確認し、本章を締めくくります。
COMはApartmentという概念を導入することで、スレッド間の実行コンテキストを定義し、これを防音壁としてスレッド同期方式を制御します。このApartmentは、1つのプロセス内でCOMオブジェクトが属することになる論理的な実行単位であり、そのオブジェクトがどのような方式で呼び出され、実行されるかを決定づける重要な基準です。
COMでは、代表的に以下の2つのThreading Modelが使用されます。

2.5.1 STA (Single-Threaded Apartment)
STA (Single-Threaded Apartment) は、1つのApartmentにただ1つのスレッドだけが所属できるモデルで、STAに属しているCOMオブジェクトは、Apartmentに属するその唯一のスレッドからのみメソッド呼び出しを受けることができます。また、COMランタイムがメッセージキューを利用してスレッドを同期し、順次処理するため、マルチスレッド環境における同期の問題を保証してくれます。つまり、COMインターフェースの実装において、開発者がスレッド同期を独自に提供する必要はありません。

2.5.2 MTA (Multi-Threaded Apartment)
MTA (Multi-Threaded Apartment) は、1つのApartmentに複数のスレッドが所属できるモデルです。このモデルに所属しているCOMオブジェクトは、複数のスレッドからの同時呼び出しを許容するため、COMサーバー側でオブジェクトに対するスレッド同期を必ず提供しなければなりません。同期を提供しなかったり、誤って実装した場合、競合状態(Race Condition)などの脆弱性につながる可能性があります。

3. 過去から学ぶ
3.1 Prior Work
ここまでCOMに関する基本的なBackgroundを学んできました。いよいよ、私たちが初めて挑戦するCOMで脆弱性を発見するために、どのようなプロセスを経たのかをご紹介します。本格的な脆弱性発見に先立ち、私たちはまず既存のCOM脆弱性に関連する先行研究を調査することにしました。学術資料を一生懸命に探したところ、特に目を引くCOM脆弱性関連の研究が2つありました。それが、USENIX Securityに掲載された Detecting Union Type Confusion in Component Object Model (以下 COMFUSION) と、COMRace: Detecting Data Race Vulnerabilities in COM Objects です。

3.2.1 COMFUSION
COMFUSIONの論文は、メモリ領域を共有するunion型と、多様なデータ型を格納できる VARIANT 構造体がCOMで使用される際、Type Confusion(型混乱)によって深刻なセキュリティ脆弱性が発生する可能性があることを指摘しています。ここで、union型は比較的馴染みがあるかもしれませんが、VARIANT 構造体は少し耳慣れないかもしれません。

VARIANTは、vtというフィールドにデータの実際の型を明示し、それに応じて解釈されるべきデータフィールドを一緒に含める構造体です。クライアントが多様な型をサポートする VARIANT 構造体を安全に使用するためには、必ず vt の値を最初に確認した上で、その型に合ったデータフィールドを読み取る必要があります。しかし、このプロセスが省略されたり、不適切に実装されたりすると、Type Confusionが発生する可能性があります。例えば、vtにchar *型を示す VT_LPSTRを保存し、データフィールドに通常の整数を保存した場合、整数をポインタとして認識してしまい、Arbitrary Address Read(任意アドレス読み取り)が発生します。

あれ?でも、Out-of-Process ServerやRemote Serverにリクエストを送信する時は、データがマーシャリング/アンマーシャリングされるため、常にデータは正しく解釈されるのではないでしょうか?
その答えは ノー です。マーシャリング/アンマーシャリングはIDLに定義された規則に従って解釈されるため、VARIANT 構造体の内部データを直接検証することはありません。つまり、COM ServerでunionおよびVARIANT構造体に対する検証が適切に行われない場合、Type Confusionの脆弱性が発生する可能性があります。

3.2.2 COMRace
COMRace 論文は、MTA環境におけるCOMオブジェクトで発生する可能性のあるRace Condition(競合状態)の脆弱性を説明しており、これを検出する特別なツールを用いて複数の脆弱性を発見しました。この研究で興味深い点は、発見された脆弱性の一部が特別なパターンを示していることでした。それは以下のようなパターンです。

このコードは、CVE-2020-1146が発生した箇所です。コードを詳しく見ると、put_AuthData メソッドがthisオブジェクトを保持した状態で、HSTRINGクラスの Set 関数を実行しています。
Set 関数では、thisに保存されたHSTRINGフィールドを WindowsDeleteString 関数によって解放し、WindowsDuplicateString 関数によって再割り当てします。
このコードには何の不具合もないように見えますが、MTA環境では2つのスレッドが同時に put_AuthData メソッドに進入して WindowsDeleteString 関数を呼び出すことで、Double-Free Bugが発生する可能性があります。

3.3 What We Learned and …
私たちはこれら2つの研究を通じて、union型と VARIANT 構造体でType Confusionが発生し得ること、そしてMTAモデルにおいてオブジェクト間でRace Conditionが発生し得ることを学びました。これら2つの研究はどちらも非常に優れた研究であると考えています。
そのように素晴らしい考察を得て次のステップに進もうとした矢先、私たちはふと、それらの脆弱性の 発見時期 を確認することになり、興味深い事実に気づきました。2つの研究で発見された脆弱性は30個以上に達していましたが、2つの脆弱性を除いて、すべて2020年に発見された(CVE-2020-XXXXX)脆弱性であることに気がつきました。
また、COMFUSIONが提示した実装ツールとCOMRaceが提示した実装ツールは、当然ながらすべて人の手を経る必要があったため、「もしかすると、先行研究者たちも見落としていた部分があるのではないか?」という疑問が生じ始めました。
こうして私たちは、COMのバグハンティングの最初の試みとして、Type ConfusionとRace Conditionという2つのキーワードを中心に新しい旅を始めることになりました。そしてその旅の終わりに、予想だにせず発見した脆弱性について、ご紹介する予定です。

4. 検出された脆弱性
先にこのバグハンティングの旅の結末を明かしてしまうと、私たちは 10個以上 の脆弱性をMSRCに報告し、CVE を獲得することができました。
4.1 Four Key Vulnerabilities
以下は、私たちが紹介する計4つのバグです。
Case 87975: Type Confusion in LxpSvc
CVE-2025-27475: Double Free Bug in InstallService
CVE-2024-49095: Use-After-Free in PrintWorkflowUserSvc
CVE-2025-21234: Improper Input Validation in PrintWorkflowUserSvc
4.1.1 Case 87975: Type Confusion in LxpSvc
該当のバグは、Windows LxpSvc (Language Experience Service) で発生した Type Confusion (型の混乱)脆弱性です。脆弱性は、内部クラスである DeviceLanguageManager の SetLanguageOperationState メソッドで発生します。SetLanguageOperationState メソッドは、計4つの引数を受け取り、その中の4番目の引数として VARIANT ポインタを渡されます。[1] と [2] で、ユーザーが入力した VARIANT の vt フィールドとデータフィールドを、v12 と v12 + 0x8 に保存します。その後、該当データは SetLanguageOperationStateInRegistry メソッドの3番目の引数として渡されて使用されます。

SetLanguageOperationStateInRegistry メソッドでは、[1] で a2 が 0 の場合、[2] で data の値をユーザーが入力した VARIANT のデータフィールドとして保存します。そして、data のアドレスを SetRegValue 関数の6番目の引数として使用します。
そして、data 値は RegSetKeyValueW 関数の5番目の引数として使用されます。

では、RegSetKeyValueW 関数とはどのような関数でしょうか?RegSetKeyValueW 関数は、指定したレジストリキーに値を生成または修正する際に使用されます。

つまり、ここでは Status というレジストリキーに、私たちが引数として渡した VARIANT のデータフィールドを 4バイト分書き込むわけです。
ところで、何かおかしいと思いませんか?これまで私たちが確認したコードには、SetLanguageOperationStateInRegistry メソッドで a2 が 0 のときに、VARIANT のデータタイプを検証する部分が存在しません。では、もし VARIANT のデータタイプが BSTR のように、ポインタを指す値だった場合はどうなるでしょうか?
まさにこの部分で、Type Confusion が発生することになります。Registry Key には 0 や 1 のような整数値が書き込まれるのではなく、データのメモリ下位4バイトのアドレスが書き込まれてしまいます。

該当のレジストリパスは、一般ユーザー権限で読み取ることができるため、下位4バイトのアドレスを流出させることができます。

個人的に、とても興味深い脆弱性だと思います。このバグについて、MSRC からは「将来的にパッチが適用される予定である」という回答を受け取りました。
4.1.2 CVE-2025-27475: Double Free Bug in InstallService
次の脆弱性は、Windows Update Stack で発見された Double Free Bug(二重解放バグ) です。脆弱性は、内部クラスである FulfillmentDataInfo の put_CrossGenSetId メソッドで発生します。
まず、脆弱な関数を呼び出すためには、InstallControl クラスの CreateFulfillmentData メソッドを呼び出して IFulfillmentDataInfo オブジェクトを取得する必要があります。このオブジェクトを取得するための条件は非常に単純です。[1] でユーザーが入力した a2 文字列の長さが 12 であるかを検証します。条件を満たした場合、[2] でオブジェクトが生成されてクライアントに返されます。
次に、取得した FulfillmentDataInfo オブジェクトの put_CrossGenSetId メソッドを確認してみましょう。[1] では this + 200 に保存されている HSTRING 変数を v3 に保存します。そして、これを [2] で解放します。ところで、このパターンはどこかで見た覚えがありませんか?
これは、まさに COMRace で紹介された CVE-2020-1146 と非常に酷似しています。5年が経過した現在でも、依然として同一パターンの脆弱性が残されていました。これにより、私たちが先行研究で抱いた疑問が確信に変わる瞬間でした。

4.1.3 CVE-2024-49095: Use-After-Free in PrintWorkflowUserSvc
私たちは Race Condition(競合状態)脆弱性を引き続き探すため、Windows Print Workflow を担当する PrintWorkflowUserSvc へと調査の旅を進めました。私たちはこのサービスでいくつかの脆弱性を発見することができました。そのうち2つの脆弱性についてご紹介します。
1つ目の脆弱性は Use-After-Free (UAF) 脆弱性です。この脆弱性は CWorkflowSession オブジェクトの SetPrintTicket メソッドで発生します。
*((_QWORD *)this + 10) には、以前 SetPrintTicket の呼び出しで割り当てられたヒープアドレスが保存されます。しかし、CWorkflowSession COM インターフェースは MTA モデルに属しているため、異なるスレッドから同時にアクセスされる可能性があります。そのため、適切なロックメカニズムが存在しないことから、レースコンディションを通じて以下のような形で Use-After-Free または Double Free が発生する可能性があります。


いくつかの制約事項により、この脆弱性の悪用(エクスプロイト)には成功しませんでしたが、いくつかのアイデアを駆使すれば、コンセプト上は悪用が可能であると思われます。
まずは ASLR bypass(ASLRの回避)です。ASLR の場合、攻撃者が制御するプロセスと、上位権限のプロセスにおける同一の DLL は同じ仮想アドレスにマッピングされるため、適切なレベルのアドレス情報を把握することができます。このようなアイデアは、IPC メカニズムを介した LPE (ローカル権限昇格) や SBX Escape (サンドボックス回避) において、最も基本となる概念です。

2つ目は、ヒープアドレスの流出です。CWorkflowSession には他のメソッドも公開されているため、このうち this + 10 のアドレスにアクセスする他のメソッドがあるかを調査しました。そしてついに、以下のような関数を見つけました。

つまり、すでに解放された this + 10 にある値を、完全に安定したサイズで Output Buffer にコピーしていることが分かります。すなわち、以下のようなシナリオで有用なヒープアドレスを取得することができます。
まず、計3つのスレッドが必要です。それぞれ図のように COM インターフェースを呼び出すことになります。スレッド 2 において、もし this + 10 に値が存在すれば、SetPrintTicket はすでに存在するヒープを解放します。その後、スレッド 3 で正常な割り当てが行われる前に、vftable を持つかヒープアドレスが存在するオブジェクトを割り当てた場合、スレッド 1 では GetPrintTicket を介して this + 10 のアドレスを参照するため、Victim(標的)オブジェクトの値を攻撃者へ正常に返すことができます。攻撃者はヒープが割り当てられるサイズフィールドを SetPrintTicket で完全に制御できるため、Victim オブジェクトのサイズ制約を受けることがなく、Race Windows の内部で解放されたホール(空き領域)へと非常に確実に配置することができます。

続いて、RIP コントロール(実行フローの制御)はどのようにして実行できるでしょうか?先ほど説明したヒープアドレス流出の方法と似たような手法で、破損した(Corrupted)vftable を作り出すことができます。
計3つのスレッドが使用されます。先ほどの方法とは異なり、2つのスレッドは SetPrintTicket を介してレースを開始します。まず this + 10 が null でなければヒープを解放します。この間に任意のオブジェクトを割り当て、該当のホールに入るようにします。次に、攻撃者が完全にコントロール可能な値とサイズで、該当の Victim オブジェクトを上書きすることができます。該当オブジェクトに vftable が存在する場合、RIP の改ざんに成功することになります。

これらのアイデアにもかかわらず、私たちは完全に動作するエクスプロイトを開発することはできませんでした。直面したいくつかの課題は以下の通りです。
仮に Race Condition を通じて解放された位置へ Victim オブジェクトを割り当てて上書きに成功したとしても、Double Free によって発生するメモリ衝突を回避することが非常に困難であり、Race Window が非常に小さいために精密なコントロールが極めて難しかったのです。

4.1.4 CVE-2025-21234: Improper Input Validation in PrintWorkflowUserSvc
いよいよ、最後の脆弱性を紹介します。前のケースと同様に、PrintWorkflowUserSvc で発生します。IPrintSupportSourceSession クラスの RequestSpoolingHandlesForWrite メソッドを見てみましょう。該当のメソッドは a2 から a7 まで計6つの引数を受け取り、すべての引数が結果値を返す Output Parameter(出力パラメータ)として使用されます。皆さんはバグを探す際、Input Parameter(入力パラメータ)が1つも存在しない関数を分析しますか?私たちの答えもまた「ノー」でした。もちろん、この脆弱性を見つけるまでは、ですが。

この関数は、[1] で this に保存されている WorkflowSessionCommon の vftable を指すアドレスを v9 に保存します。そして、該当のアドレスを引数として、[2] で WorkflowSessionCommon クラスの RequestSpoolingHandlesForWrite メソッドを呼び出します。

このメソッドでは、[1] でクライアントの PID を取得し、[2] でクライアントプロセスのハンドルを取得します。その後、[3] で v22 に this を参照して何らかの値を保存し、[4] で DuplicateHandle 関数を呼び出します。おや? DuplicateHandle 関数ですか?

DuplicateHandle 関数は、オブジェクトのハンドルを複製する Windows API です。関数の引数に関する説明を要約すると、第1引数と第2引数に、複製元となるハンドルを所有するプロセスのハンドルと、複製対象のハンドルが入ります。そして第3引数には複製されたハンドルを受け取るプロセスのハンドルが入り、第4引数に複製されたハンドルの値が返されます。MSDNの説明によると、PrintWorkflowUserSvc の v22 ハンドルを、クライアントプロセスにコピーすることを意味します。COMにおいてこのようなパターンは稀に見られますが、通常はコールバックやメソッドの実装のためにイベントハンドルをクライアントプロセスに返すケースが多いです。

v22 値が実際にどこでセットされているかは、簡単なリバースエンジニアリングを通じて確認することができました。
該当の値は、PrintSupportSession クラスのコンストラクタで初期化されていることが確認されました。v22 は -1 に初期化されます。Windows において -1 というハンドルは、自分自身を指す疑似ハンドル(Pseudo Handle)の値を意味します。

すなわち、DuplicateHandle 関数において、自身のプロセスハンドルをクライアントプロセスに対して複製していることを意味します。ということは、PrintWorkflowUserSvc よりも低い権限を持つクライアントがこのメソッドを呼び出すと、PrintWorkflowUserSvc のハンドルを自身のプロセスとして複製できてしまいます。
どのようなプロセスが PrintWorkflowUserSvc にアクセスできるのか、このサービスの Access Permission (アクセス権限) を確認してみました。その結果、この COM オブジェクトはすべての UWP アプリや、特定の Capability を持つ Low Privileged AppContainer(低権限アプリコンテナ)からもアクセス可能であることを確認しました。これは、サンドボックス化されたプロセスからでも PrintWorkflowUserSvc のプロセスハンドルを自プロセスに複製できることを意味します。

この脆弱性は、本当に完璧なロジックバグです。権限昇格のために、前述したような複雑な Use-After-Free 脆弱性を悪用(エクスプロイト)する必要は全くありません。あとは、複製されたハンドルを利用して PrintWorkflowUserSvc のメモリにシェルコードを書き込み、それを CreateRemoteThread API を利用して実行するだけです。

4.4.3 Exploit Demo
脆弱性を実証するため、Adobe Acrobat のサンドボックス内で実行されるレンダラーから任意コード実行が遂行可能であると仮定し、シェルコードを注入して Sandbox Escape(サンドボックス回避)を実行しました。これにより、AppContainer から Medium 権限への昇格に成功しました。
驚くべきことに、このバグは MSRC から “Exploit Less Likely(悪用の可能性は低い)” と判定されました。私たちは未だに MSRC がなぜこのような判断を下したのかよく分かっていません。

5. 結果
最後に結論です。
最終的に、私たちは2024年から最近まで、Type ConfusionやRace Conditionの脆弱性を含め、10件以上の脆弱性をMicrosoftに報告し、CVEとして認定されることができました。

このような成果を上げることができたのは、先行研究者の方々の情報共有と、oleviewdotnetという非常に優れたツールのおかげでした。特に、COM脆弱性分析を開始するための基盤を築いてくださった先行研究者の方々と、James Forshaw氏に深く感謝の意を表します。

最後に、今回の旅を終えるにあたって感じた考えを共有し、締めくくらせていただきます。
新しい分野で新しい脆弱性を発見するためには、先行研究に対する徹底的な分析が不可欠であると考えています。単に既存の研究をなぞるだけにとどまらず、その中でまだ扱われていない部分や、拡張可能な可能性に注目することが重要であるという点をお伝えしたいです。
心残りな点もありました。特に、複数のRace Condition脆弱性を発見したにもかかわらず、完全なエクスプロイトにまで至らなかった点は、今後私たちがさらに補うべき部分であると考えています。
私たちのFuture Workは、このような脆弱性パターンをより体系的に検出できるツールを開発し、それを通じてさらに多くの脆弱性を発見することです。私たちの旅はこれからも続いていきます。

人気の記事








