今日表示しない

トップへ戻る

トップへ戻る

Windowsバグハンティングのサムネイル
Windowsバグハンティングのサムネイル

脆弱性研究

脆弱性研究

脆弱性研究

Windows | COM-pletely Unplanned: LPEWindows

Windows | COM-pletely Unplanned: LPEWindows

Windows | COM-pletely Unplanned: LPEWindows

エンキホワイトハット

エンキホワイトハット

コンテンツ

コンテンツ

コンテンツ

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個のバグのチェーン化が必須となっています。


운영체제 점유율을 보여주는 통계 그래프로, Windows가 72.88%로 가장 높고, 그 뒤를 OS X(15.16%), Linux(4.11%) 등이 따르고 있습니다.


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

https://cloud.google.com/blog/topics/threat-intelligence/2024-zero-day-trends

このような流れの中で、従来とは異なる権限昇格の経路を探そうとする試みが自然と登場しています。私たちも同様に、伝統的なカーネル攻撃ベクトル以外の経路で権限昇格が可能な構造が残っているのではないかという疑問から、研究を開始することになりました。

その過程で私たちが注目したのは、低い権限からアクセスできるものの、内部的には呼び出し元よりも高い権限で動作するコンポーネントでした。特に以下のような特性を持つ対象を、主な探索候補として選定しました:

  1. SYSTEM権限で実行されるサービス

  2. 一般ユーザー(Normal Integrity User)やLow Privileged AppContainerのような制限された権限からでもアクセス可能なサービス

これらの条件をベースに、Windowsのアーキテクチャを分析していたところ、ある興味深い構造が目に留まりました。それがCOM (Component Object Model)でした。

実は、最初からCOMをターゲットにしようとしていたわけではありませんでした。COMで構成されたWindows Runtimeをサポートする一部のサービスと、その他のLocalサービスが、低い権限(User、AppContainerなど)から高い権限を持つプロセスへ直接リクエストを送信できることを発見し、この小さな手がかりが私たちの研究の方向性を決定づけました。

そうして、まったく計画にはありませんでしたが、素晴らしい成果を上げることができた「COM-pletely Unplanned」な旅が始まりました。


주먹을 쥔 아기 밈(Success Kid) 이미지와 'YEAH! WE FOUND COM!' 문구.

2. COM

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


We have COM :D' 말풍선과 COM 기반 윈도우 및 오피스 프로그램 아이콘들.

2.1 Basic Concepts of COM

まず、COMの脆弱性分析を開始する前に、COMが何であるかを正確に理解する必要がありました。一緒に簡単に紐解いてみましょう。

COMはMicrosoftが設計したコンポーネントベースのソフトウェアアーキテクチャで、異なるプロセスやモジュール間でオブジェクトを作成し、データを送受信できるようにします。特にCOMの最大の強みの1つは、さまざまなプログラミング言語でCOMオブジェクトを作成できる点です。COMは、C/C++、C#、PowerShell、Pythonなどの多様な言語から同じ方法でアクセスできるインターフェースを提供します。

例として、以下のようにPowerShellとPythonを使用し、言語に依存しない共通の方法でExcel(およびその他のWindowsコンポーネント)にデータを入力することができます。


PowerShell과 Python에서 각각 COM을 이용해 Excel A1 셀에 'Hello COM'을 입력하는 코드 및 실행 결과 화면 비교.

しかし、これはどのようにして可能になっているのでしょうか?

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


개발 도구에 표시된 'ApplicationLicenseManager' COM 클래스와 관련 인터페이스 ID(IID) 목록.

2.2 COM Server Models

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


포켓몬 캐릭터로 비유한 COM 서버 모델: In-Process, Out-of-Process, Remote Process.
  1. In-Process Server (InProcServer) 方式は、COMサーバーがDLL形式で実装されています。この方式でCOMオブジェクトを使用すると、クライアントと同じプロセス内で実行されるため、クライアントと同じ権限を持つことになります。簡単に言えば、自分のプロセスがCOMサーバーの実装されているDLLをロードして使用するようなものです。

  2. Out-of-Process Server (LocalServer) 方式は、COMサーバーがEXE形式で実装されています。この方式は、クライアントとは別個のプロセスで実行されるため、COMサーバープロセスのセキュリティ設定によって権限が異なります。spoolsv.exeやmsiserver.exeのように、固有の名前を持つプロセス内にCOMサーバーが実装されている場合もありますが、多くはWindowsでサービスホスティングを担当するsvchost.exeが、COMサーバーの実装されたDLLをロードして使用します。

    この方式は、クライアントが別のプロセスにリクエストを送信するため、Out-of-Process ServerではProxyとStubの構造を使用します。この構造については後ほど説明します。

  3. 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サーバーは起動されますが、オブジェクトにアクセスしてメソッドを呼び出すことはできません。


COM 실행 권한만 있고 접근 권한은 없어 COM 객체 사용이 제한되는 상황을 표현한 밈.


2.4 COM Data Transfer

さて、ここまでCOMのサーバーモデルとセキュリティモデルについて簡単に見てきました。今回は、COMがどのようにデータを送受信しているのかについて見ていきましょう。

COMは前述したIn-Process Server方式を使用することで、単一プロセス内でもオブジェクト間通信を可能にしますが、Out-of-Process ServerやRemote Serverの場合は、プロセスやシステムが互いに異なるため、データ転送のための追加プロセスが必要です。まさにこの時に使用されるのが、Proxy/Stubとマーシャリング/アンマーシャリングです。


COM 프록시, COM 스텁, 마샬링 개념 아이콘.

2.4.1 Proxy/Stub

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


COM 프록시와 스텁 간의 데이터 마샬링 및 언마샬링 과정.

2.4.2 Marshalling/Unmarshalling

マーシャリングとは、プロセスのメモリ空間に存在するオブジェクトデータを、他のプロセスでも使用できるようにシリアル化するプロセスです。COMでは、このプロセスを介して呼び出したいインターフェースと引数をIDL(Interface Definition Language)で定義されたルールに沿って変換し、これをRPCベースで送信します。

一般的に、シリアル化はオブジェクトのデータを単なるバイトストリームに変換するプロセスと理解されますが、COMにおけるマーシャリングははるかに広い概念です。COMのマーシャリングは基本的に、メソッドの基本的な呼び出し引数(int、BSTRなど)を含め、IUnknown *、IInspectable *のようなインターフェースポインタなどを1つの呼び出しコンテキストとしてまとめて送信します。

逆にアンマーシャリングは、このようにマーシャリングされたデータをもとのデータに変換し、サーバー側で実際のオブジェクトデータとして使用できるようにするプロセスです。繰り返しになりますが、アンマーシャリングされたデータは、マーシャリング前のデータと同一の構造でなければならず、このためにマーシャリングとアンマーシャリングの双方が、IDLに定義されたデータ形式と順序に厳格に従う必要があります。もしこのルールに従わない場合、RPCランタイムで例外が発生したり、データの解釈に失敗したりします。


클라이언트, IDL 인터페이스 정의, 원격 COM 객체 간의 관계도.

2.5 COM Threading Model

最後に、COMのThreading Modelについて簡単に確認し、本章を締めくくります。

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

COMでは、代表的に以下の2つのThreading Modelが使用されます。


COM의 STA (단일 스레드) 모델과 MTA (다중 스레드) 모델 비교 다이어그램.

2.5.1 STA (Single-Threaded Apartment)

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


COM 단일 스레드 아파트먼트(STA) 모델: 하나의 스레드만 COM 객체에 접근, 나머지는 대기.

2.5.2 MTA (Multi-Threaded Apartment)

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


COM 다중 스레드 아파트먼트(MTA) 모델: 여러 스레드가 동시에 COM 객체에 접근.

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 です。


USENIX 보안 학회에 발표된 COMFUSION과 COMRace 논문 표지


3.2.1 COMFUSION

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


union 타입을 잘 아는 강한 도지(Swole Doge)와 VARIANT 타입을 잘 모르는 약한 도지(Cheems) 비교 밈

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の脆弱性が発生する可能性があります。


POV: VARIANT 타입 체크를 건너뛸 때' 문구와 불타는 집 배경의 소녀(재앙의 소녀) 밈.


3.2.2 COMRace

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


USENIX 보안 학회 발표 논문 'COMRACE: COM 객체의 데이터 경쟁 취약점 탐지' 표지.

このコードは、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 (型の混乱)脆弱性です。脆弱性は、内部クラスである DeviceLanguageManagerSetLanguageOperationState メソッドで発生します。SetLanguageOperationState メソッドは、計4つの引数を受け取り、その中の4番目の引数として VARIANT ポインタを渡されます。[1] と [2] で、ユーザーが入力した VARIANTvt フィールドとデータフィールドを、v12 v12 + 0x8 に保存します。その後、該当データは SetLanguageOperationStateInRegistry メソッドの3番目の引数として渡されて使用されます。


DeviceLanguageManager::SetLanguageOperationState 함수 코드: VARIANT 타입과 데이터를 가져와 하위 함수를 호출하는 부분.

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

そして、data 値は RegSetKeyValueW 関数の5番目の引数として使用されます。

SetLanguageOperationStateInRegistry 함수에서 처리된 데이터가 SetRegValue 함수로 전달되는 과정 다이어그램.

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

RegSetKeyValueW Windows API 함수 정의 및 주요 매개변수(lpData, cbData) 설명.

つまり、ここでは Status というレジストリキーに、私たちが引数として渡した VARIANT のデータフィールドを 4バイト分書き込むわけです。

ところで、何かおかしいと思いませんか?これまで私たちが確認したコードには、SetLanguageOperationStateInRegistry メソッドで a2 が 0 のときに、VARIANT のデータタイプを検証する部分が存在しません。では、もし VARIANT のデータタイプが BSTR のように、ポインタを指す値だった場合はどうなるでしょうか?

まさにこの部分で、Type Confusion が発生することになります。Registry Key には 0 や 1 のような整数値が書き込まれるのではなく、データのメモリ下位4バイトのアドレスが書き込まれてしまいます。

LxpSvc의 SetLanguageOperationStateInRegistry 함수 코드 일부: 사용자 VARIANT 값의 조건부 처리 및 SetRegValue 호출 부분.

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

타입 혼란으로 인한 힙 주소 일부 유출(디버거/레지스트리)과 '괜찮아 울지마' 토이스토리 위로 밈.

個人的に、とても興味深い脆弱性だと思います。このバグについて、MSRC からは「将来的にパッチが適用される予定である」という回答を受け取りました。

4.1.2 CVE-2025-27475: Double Free Bug in InstallService

次の脆弱性は、Windows Update Stack で発見された Double Free Bug(二重解放バグ) です。脆弱性は、内部クラスである FulfillmentDataInfoput_CrossGenSetId メソッドで発生します。

まず、脆弱な関数を呼び出すためには、InstallControl クラスの CreateFulfillmentData メソッドを呼び出して IFulfillmentDataInfo オブジェクトを取得する必要があります。このオブジェクトを取得するための条件は非常に単純です。[1] でユーザーが入力した a2 文字列の長さが 12 であるかを検証します。条件を満たした場合、[2] でオブジェクトが生成されてクライアントに返されます。

// Hidden C++ exception states: #wind=63
__int64 __fastcall WindowsUpdate::Internal::InstallControl::CreateFulfillmentData(
        WindowsUpdate::Internal::InstallControl *this,
        HSTRING a2,
        struct WindowsUpdate::Internal::IFulfillmentDataInfo **a3)
{
  HSTRING v5; // [rsp+38h] [rbp+10h] BYREF

  v5 = a2;
  if ( Utils::IsValidProductId(a2, a2) ) // [1] Verify that a2 is 0xC bytes 
    return Microsoft::WRL::Details::MakeAndInitialize<WindowsUpdate::Internal::FulfillmentDataInfo,WindowsUpdate::Internal::IFulfillmentDataInfo,HSTRING__ *>(
             a3,
             &v5); // [2] Return IFulfillmentDataInfo object
  *a3 = 0LL;
  return 2147942487LL;
}
// Hidden C++ exception states: #wind=63
__int64 __fastcall WindowsUpdate::Internal::InstallControl::CreateFulfillmentData(
        WindowsUpdate::Internal::InstallControl *this,
        HSTRING a2,
        struct WindowsUpdate::Internal::IFulfillmentDataInfo **a3)
{
  HSTRING v5; // [rsp+38h] [rbp+10h] BYREF

  v5 = a2;
  if ( Utils::IsValidProductId(a2, a2) ) // [1] Verify that a2 is 0xC bytes 
    return Microsoft::WRL::Details::MakeAndInitialize<WindowsUpdate::Internal::FulfillmentDataInfo,WindowsUpdate::Internal::IFulfillmentDataInfo,HSTRING__ *>(
             a3,
             &v5); // [2] Return IFulfillmentDataInfo object
  *a3 = 0LL;
  return 2147942487LL;
}
// Hidden C++ exception states: #wind=63
__int64 __fastcall WindowsUpdate::Internal::InstallControl::CreateFulfillmentData(
        WindowsUpdate::Internal::InstallControl *this,
        HSTRING a2,
        struct WindowsUpdate::Internal::IFulfillmentDataInfo **a3)
{
  HSTRING v5; // [rsp+38h] [rbp+10h] BYREF

  v5 = a2;
  if ( Utils::IsValidProductId(a2, a2) ) // [1] Verify that a2 is 0xC bytes 
    return Microsoft::WRL::Details::MakeAndInitialize<WindowsUpdate::Internal::FulfillmentDataInfo,WindowsUpdate::Internal::IFulfillmentDataInfo,HSTRING__ *>(
             a3,
             &v5); // [2] Return IFulfillmentDataInfo object
  *a3 = 0LL;
  return 2147942487LL;
}

次に、取得した FulfillmentDataInfo オブジェクトの put_CrossGenSetId メソッドを確認してみましょう。[1] では this + 200 に保存されている HSTRING 変数を v3 に保存します。そして、これを [2] で解放します。ところで、このパターンはどこかで見た覚えがありませんか?

__int64 __fastcall WindowsUpdate::Internal::FulfillmentDataInfo::put_CrossGenSetId(
        WindowsUpdate::Internal::FulfillmentDataInfo *this,
        HSTRING a2)
{
  unsigned int v2; // ebx
  HSTRING *v3; // rdi

  v2 = 0;
  v3 = (HSTRING *)((char *)this + 200); // [1] Get HSTRING from this + 200
  if ( !a2 || a2 != *v3 )
  {
    WindowsDeleteString(*v3); // [2] Free HSTRING
    *v3 = 0LL;
    return (unsigned int)WindowsDuplicateString(a2, v3);
  }
  return v2;
}
__int64 __fastcall WindowsUpdate::Internal::FulfillmentDataInfo::put_CrossGenSetId(
        WindowsUpdate::Internal::FulfillmentDataInfo *this,
        HSTRING a2)
{
  unsigned int v2; // ebx
  HSTRING *v3; // rdi

  v2 = 0;
  v3 = (HSTRING *)((char *)this + 200); // [1] Get HSTRING from this + 200
  if ( !a2 || a2 != *v3 )
  {
    WindowsDeleteString(*v3); // [2] Free HSTRING
    *v3 = 0LL;
    return (unsigned int)WindowsDuplicateString(a2, v3);
  }
  return v2;
}
__int64 __fastcall WindowsUpdate::Internal::FulfillmentDataInfo::put_CrossGenSetId(
        WindowsUpdate::Internal::FulfillmentDataInfo *this,
        HSTRING a2)
{
  unsigned int v2; // ebx
  HSTRING *v3; // rdi

  v2 = 0;
  v3 = (HSTRING *)((char *)this + 200); // [1] Get HSTRING from this + 200
  if ( !a2 || a2 != *v3 )
  {
    WindowsDeleteString(*v3); // [2] Free HSTRING
    *v3 = 0LL;
    return (unsigned int)WindowsDuplicateString(a2, v3);
  }
  return v2;
}

これは、まさに COMRace で紹介された CVE-2020-1146 と非常に酷似しています。5年が経過した現在でも、依然として同一パターンの脆弱性が残されていました。これにより、私たちが先行研究で抱いた疑問が確信に変わる瞬間でした。

Windows::System::Internal::SignInContext::put_AuthData(*this, HSTRING a2) {
    Microsoft::WRL::Wrappers::HString::Set(this + 15, &a2);
}

__int64 HString::Set(HSTRING *newString, HSTRING *a2) {
    unsigned int v2;
    v2 = 0;
    if(!*a2 || *a2 != *newString) {
        WindowsDeleteString(*newString); // Delete
        *newString = 0;
        v2 = WindowsDuplicateString(*a2, newString);
    }
}
Windows::System::Internal::SignInContext::put_AuthData(*this, HSTRING a2) {
    Microsoft::WRL::Wrappers::HString::Set(this + 15, &a2);
}

__int64 HString::Set(HSTRING *newString, HSTRING *a2) {
    unsigned int v2;
    v2 = 0;
    if(!*a2 || *a2 != *newString) {
        WindowsDeleteString(*newString); // Delete
        *newString = 0;
        v2 = WindowsDuplicateString(*a2, newString);
    }
}
Windows::System::Internal::SignInContext::put_AuthData(*this, HSTRING a2) {
    Microsoft::WRL::Wrappers::HString::Set(this + 15, &a2);
}

__int64 HString::Set(HSTRING *newString, HSTRING *a2) {
    unsigned int v2;
    v2 = 0;
    if(!*a2 || *a2 != *newString) {
        WindowsDeleteString(*newString); // Delete
        *newString = 0;
        v2 = WindowsDuplicateString(*a2, newString);
    }
}
또 COMRACE네, 같은 패턴이야' 문구와 놀란 표정의 피카츄 밈.

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 が発生する可能性があります。

CWorkflowSession::SetPrintTicket 함수 코드: 힙 메모리 해제 및 재할당, 데이터 복사 로직 강조.SetPrintTicket 메소드에서의 Use-After-Free 및 Double-Free 발생 시나리오 비교 다이어그램


いくつかの制約事項により、この脆弱性の悪用(エクスプロイト)には成功しませんでしたが、いくつかのアイデアを駆使すれば、コンセプト上は悪用が可能であると思われます。

まずは ASLR bypass(ASLRの回避)です。ASLR の場合、攻撃者が制御するプロセスと、上位権限のプロセスにおける同一の DLL は同じ仮想アドレスにマッピングされるため、適切なレベルのアドレス情報を把握することができます。このようなアイデアは、IPC メカニズムを介した LPE (ローカル権限昇格) や SBX Escape (サンドボックス回避) において、最も基本となる概念です。

두 msedge.exe 프로세스에서 ntdll.dll이 동일한 기준 주소에 매핑된 속성 창 (ASLR 우회 설명용)


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

CWorkflowSession::GetPrintTicket 함수 코드: 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 の改ざんに成功することになります。

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つも存在しない関数を分析しますか?私たちの答えもまた「ノー」でした。もちろん、この脆弱性を見つけるまでは、ですが。

IWorkflowSourceSession 인터페이스 IDL: 다수의 출력 파라미터를 가진 Proc14 메소드 강조 표시.

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

IPrintSupportSourceSession::RequestSpoolingHandlesForWrite 함수 코드: WorkflowSessionCommon의 같은 이름 함수를 호출하는 래퍼.

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

WorkflowSessionCommon::RequestSpoolingHandlesForWrite 함수 코드: DuplicateHandle을 호출하여 프로세스 핸들을 복제하는 로직.

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

DuplicateHandle API 인수 설명: v22(-1, 현재 프로세스 핸들)가 복제될 소스 핸들로 사용됨을 시각화.

v22 値が実際にどこでセットされているかは、簡単なリバースエンジニアリングを通じて確認することができました。

該当の値は、PrintSupportSession クラスのコンストラクタで初期化されていることが確認されました。v22-1 に初期化されます。Windows において -1 というハンドルは、自分自身を指す疑似ハンドル(Pseudo Handle)の値を意味します。

PrintSupportSession 생성자 C++ 코드: 객체 멤버 v22 (this + 0xB0)를 -1로 초기화하는 부분.

すなわち、DuplicateHandle 関数において、自身のプロセスハンドルをクライアントプロセスに対して複製していることを意味します。ということは、PrintWorkflowUserSvc よりも低い権限を持つクライアントがこのメソッドを呼び出すと、PrintWorkflowUserSvc のハンドルを自身のプロセスとして複製できてしまいます。

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

COM 객체 ACL 설정: 'ALL APPLICATION PACKAGES' 등 AppContainer에 실행/활성화 권한 부여됨.

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

when we tried a race condition exploit

4.4.3 Exploit Demo

脆弱性を実証するため、Adobe Acrobat のサンドボックス内で実行されるレンダラーから任意コード実行が遂行可能であると仮定し、シェルコードを注入して Sandbox Escape(サンドボックス回避)を実行しました。これにより、AppContainer から Medium 権限への昇格に成功しました。

驚くべきことに、このバグは MSRC から “Exploit Less Likely(悪用の可能性は低い)” と判定されました。私たちは未だに MSRC がなぜこのような判断を下したのかよく分かっていません。

Microsoft 보안 업데이트 가이드의 '악용 가능성 평가: 가능성 낮음(Exploitation Less Likely)' 화면.

5. 結果

最後に結論です。

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

연구팀이 보고한 CVE 및 취약 사례 목록표 (CVE ID, 컴포넌트, 영향도 명시).

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


COMRACE 논문과 oleviewdotnet GitHub 저장소 화면 (선행 연구 및 도구 감사 표시).

最後に、今回の旅を終えるにあたって感じた考えを共有し、締めくくらせていただきます。

新しい分野で新しい脆弱性を発見するためには、先行研究に対する徹底的な分析が不可欠であると考えています。単に既存の研究をなぞるだけにとどまらず、その中でまだ扱われていない部分や、拡張可能な可能性に注目することが重要であるという点をお伝えしたいです。

心残りな点もありました。特に、複数のRace Condition脆弱性を発見したにもかかわらず、完全なエクスプロイトにまで至らなかった点は、今後私たちがさらに補うべき部分であると考えています。

私たちのFuture Workは、このような脆弱性パターンをより体系的に検出できるツールを開発し、それを通じてさらに多くの脆弱性を発見することです。私たちの旅はこれからも続いていきます。

エンキホワイトハット

エンキホワイトハット

ENKI ホワイトハット
ENKI ホワイトハット

オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。

オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。

隙のないセキュリティ設計の始まり、NO.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

購読する

コンテンツが役に立ったら?
エンキーレターを購読しましょう!

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.