
취약점 연구
엔키화이트햇
2025. 5. 12.
1. Introduction
안녕하세요. 우리는 ENKI WhiteHat의 보안 연구원이자 Windows Bug Hunter인 김종성, 김동준입니다. 우리는 2024년부터 2025년 최근까지 윈도우에서 권한 상승 취약점을 찾기 위한 연구를 진행하였습니다.
우리는 윈도우에서 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, AppContaine 등) 에서 높은 권한을 가진 프로세스에게 요청을 직접적으로 보낼 수 있다는 점을 발견했고, 이 작은 단서가 우리의 연구의 방향을 정해주었습니다.
그렇게, 전혀 계획에 없었지만 좋은 결과를 낼 수 있었던, “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의 가장 큰 장점 중 하나는, 다양한 프로그래밍 언어로 COM 개체를 만들 수 있다는 점입니다. COM은 C/C++, C#, PowerShell, Python 등 다양한 언어에서 동일한 방식으로 접근할 수 있는 인터페이스를 제공합니다.
예시로, 아래와 같이 PowerShell과 Python을 이용해 언어에 구애받지 않고, 공통된 방식으로 Excel (기타 Windows Components) 에 데이터를 입력할 수 있습니다.

그런데 이것이 어떻게 가능한 것일까요?
COM은 객체의 식별을 위해 CLSID (Class ID) 와 IID (Interface ID) 를 사용합니다. CLSID는 COM 클래스의 고유 식별자이며, 레지스트리에 등록되어 해당 객체를 생성할 수 있는 위치 및 방법을 정의합니다. IID는 해당 COM 클래스가 제공하는 각 인터페이스의 고유 식별자입니다. 이로써, 클라이언트는 특정 CLSID를 이용해 객체를 생성하고, 해당 객체가 제공하는 인터페이스를 IID를 통해 사용할 수 있게 되며, 다른 언어로 COM 개체를 호출해도 동일한 바이너리 인터페이스를 공유합니다.

2.2 COM Server Models
그렇다면, 아까 예시처럼 COM을 통해 Excel의 기능을 사용할 수 있다면 이 COM 객체를 실제로 어디서 실행되며 어떤 권한으로 동작하는지는 어떻게 결정될까요? COM 서버는 실행 방식에 따라 다음과 같이 크게 세 가지로 나뉩니다.

In-Process Server (InProcServer) 방식은 COM Server가 DLL 형태로 구현되어 있습니다. 이 방식으로 COM 개체를 사용하면 클라이언트와 동일한 프로세스 내에서 실행되기 때문에 클라이언트와 같은 권한을 갖게 됩니다. 간단히 말해, 내 프로세스가 COM Server가 구현되어 있는 DLL을 로드하여 사용하는 것과 같습니다.
Out-of-Process Server (LocalServer) 방식은 COM Server가 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 Server를 마음대로 실행하고, 인터페이스에서 제공하는 메소드를 호출할 수 있다면, 이는 심각한 권한 상승과 원격 코드 실행으로 이어질 수 있습니다. 이를 방지하기 위해 COM은 Launch 권한과 Access 권한이라는 두 가지 중요한 보안 체크를 수행합니다.
2.3.1 Launch Permission
Launch Permission은 클라이언트 애플리케이션이 해당 COM 서버를 새롭게 실행 (Activation) 할 수 있는 지를 판단합니다.
예시로 어떤 사용자가 특정 COM Class의 대한 Launch Permission을 갖고 있다면, CoCreateInstance와 같은 함수를 통해 해당 COM Server를 새롭게 실행할 수 있습니다.
그렇다면 Access Permission과의 차이점은 무엇일까요?
2.3.2 Access Permission
Access Permission은 이미 실행 중인 COM 서버에 대해, 클라이언트가 해당 COM 객체의 메소드를 호출할 수 있는 지를 판단합니다. 해당 권한을 갖고 있다면, 이미 실행되고 있는 COM 서버에 접근하여 객체가 노출한 인터페이스의 메소드 호출 권한을 얻게 됩니다.
반대로 Launch Permission만 있고 Access Permission이 없다면, COM 서버는 실행되지만, 객체에 접근하여 메소드를 호출하는 것은 불가능합니다.

2.4 COM Data Transfer
자 이제, 우리는 지금까지 COM의 Server 모델과 보안 모델에 대해 간단하게 알아보았습니다. 이번에는 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 *과 같은 인터페이스 포인터 등을 하나의 호출 컨텍스트로 묶어 전송합니다.
반대로 언마샬링은 이렇게 마샬링된 데이터를 원본 데이터로 바꿔, 서버 측에서 실제 객체 데이터처럼 사용할 수 있도록 만드는 과정입니다. 다시 한번 강조하지만, 언마샬링된 데이터는 마샬링 이전의 데이터와 동일한 구조여야 하며, 이를 위해 마샬링과 언마샬링 모두 IDL에 정의된 데이터 형식 및 순서를 정확히 따라야 합니다. 만약 이 규칙을 따르지 않으면, RPC 런타임에서 예외가 발생하거나 데이터 해석에 실패하게 됩니다.

2.5 COM Threading Model
마지막으로 COM의 Threading Model에 대해 간단히 살펴본 뒤, 본 챕터를 마무리하겠습니다.
COM은 Apartment라는 개념을 도입해 스레드 간의 실행 컨텍스트를 정의하고, 이를 기반으로 스레드 동기화 방식을 제어합니다. 이 Apartment는 하나의 프로세스 내에서 COM 객체가 속하게 되는 논리적인 실행 단위로, 해당 객체가 어떤 방식으로 호출되고 실행될 지를 결정짓는 중요한 기준입니다.
COM에서는 대표적으로 아래 두 가지 Threading Model이 사용됩니다.

2.5.1 STA (Single-Threaded Apartment)
STA (Single-Threaded Apartment) 는 한 Apartment에 오직 하나의 스레드만 소속될 수 있는 모델로, STA에 속해있는 COM 개체는 Apartment에 속한 하나의 스레드에서만 메소드 호출을 받을 수 있습니다. 또한 COM Runtime이 메시지 큐를 이용하여 스레드를 동기화하고 순차적으로 처리하기 때문에 멀티 스레딩 환경에서 동기화 문제를 보장해 줍니다. 즉, COM 인터페이스 구현에서 스레드 동기화를 제공할 필요가 없습니다.

2.5.2 MTA (Multi-Threaded Apartment)
MTA (Multi-Threaded Apartment) 는 한 Apartment에 여러 스레드가 소속될 수 있는 모델입니다. 이 모델에 소속되어 있는 COM 객체는 여러 스레드로부터의 동시 호출을 허용하기 때문에, COM Server 측에서 객체에 대한 스레드 동기화를 반드시 제공해야 합니다. 만약 동기화를 제공하지 않거나, 잘못 구현했을 경우 이는 Race Condition과 같은 취약점으로 이어질 수 있습니다.

3. Learning from the Past
3.1 Prior Work
지금까지 COM에 대한 기본적인 Background을 알아봤습니다. 이제 우리가 처음 도전해보는 COM에서 취약점 발견을 하기 위해 어떤 과정을 거쳤는지 알아보겠습니다. 우리는 본격적인 취약점 발견에 앞서, 기존에 COM 취약점과 관련된 선행 연구를 먼저 살펴보기로 했습니다. 우리는 열심히 학술 자료를 찾아보았고, 눈에 띄인 COM 취약점 관련 연구가 두 개가 있었습니다. 바로 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 환경에서는 두 스레드가 동시에 put_AuthData
메소드에 진입하고 WindowsDeleteString
함수를 호출하여, Double-Free Bug가 발생될 수 있습니다.

3.3 What We Learned and …
우리는 두 연구를 통해 union type과 VARIANT 구조체에서 Type Confusion이 발생할 수 있다는 점과, MTA 모델에서 객체 간의 Race Condition이 발생될 수 있다는 점을 알게 되었습니다. 우리는 이 두 연구가 모두 매우 훌륭한 연구라고 생각합니다.
그렇게 좋은 인사이트를 얻었다고 넘어가려던 찰나, 우리는 문득 해당 취약점들의 발견 시점을 확인하게 되었고, 흥미로운 사실을 알게 되었습니다. 두 연구에서 발견한 취약점은 30개 이상에 달했지만, 두 개의 취약점을 제외하고는 모두 2020년에 발견된 (CVE-2020-XXXXX) 취약점이라는 것을 알게 되었습니다.
또한 COMFUSION에서 제시한 구현체와 COMRace에서 제시한 구현체는, 당연하게도 모두 사람의 손을 거쳐야 했기 때문에, 혹시 선행 연구자들도 무언가 지나쳤던 부분이 있지 않을까? 라는 의문이 들기 시작했습니다.
그렇게 우리는, COM 버그 헌팅의 첫 시도로 Type Confusion과 Race Condition 이라는 두 키워드를 중심으로 새로운 여정을 시작하게 되었습니다. 그리고 그 여정의 끝에서, 예상치 못하게 발견한 취약점을, 소개할 예정입니다.

4. Discovered Vulnerabilities
미리 이 버그 헌팅 여정의 끝을 스포하자면, 우리는 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
메소드는, 총 네 개의 인자를 받으며, 그중 네 번째 인자로 VARIANT 포인터를 전달 받습니다. [1]과 [2]에서 사용자가 입력한 VARIANT의 vt
필드와 데이터 필드를, v12
와 v12 + 0x8
에 저장합니다. 이후 해당 데이터는SetLanguageOperationStateInRegistry
메소드의 세 번째 인자로 전달되어 사용됩니다.

SetLanguageOperationStateInRegistry
메소드에서는 [1]에서 a2가 0이면, [2]에서 data
의 값을 사용자가 입력한 VARIANT의 데이터 필드로 저장합니다. 그리고 data
의 주소를 SetRegValue
함수의 여섯 번째 인자로 사용합니다.
그리고, data
값은 RegSetKeyValueW
함수의 다섯 번째 인자로 사용됩니다.

그렇다면 RegSetKeyValueW
함수는 어떤 함수일까요? RegSetKeyValueW
함수는, 지정한 레지스트리 키에 값을 생성하거나 수정할 때 사용됩니다.

즉, 여기서는 Status
라는 레지스트리 키에 우리가 인자로 전달한 VARIANT의 데이터 필드를 4byte 만큼 적는 것이죠.
그런데 무언가 이상하지 않나요? 지금까지 우리가 봤던 코드에는 SetLanguageOperationStateInRegistry
메소드에서 a2
가 0일 때, VARIANT의 데이터 타입을 검증하는 부분이 존재하지 않습니다. 그렇다면 만약, VARIANT의 데이터 타입이 BSTR과 같이, 포인터를 가리키는 값이면 어떻게 될까요?
바로 이 부분에서, Type Confusion이 발생하게 됩니다. Registry Key에는 0과 1 같은 정수 값이 적히는 것이 아닌, 데이터의 하위 4byte 주소가 적히게 됩니다.

해당 레지스트리 경로는, 일반 유저 권한에서 읽을 수 있기 때문에, 하위 4byte 주소를 유출할 수 있습니다.

개인적으로, 재미있는 취약점인 거 같습니다. 이 버그는 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로 여정을 떠났습니다. 우리는 이 서비스에서 다양한 취약점을 발견할 수 있었습니다. 그 중 두 개의 취약점에 대해 소개하겠습니다.
첫 번째 취약점은 Use-After-Free 취약점입니다. 취약점은 CWorkflowSession
객체의 SetPrintTicket
메소드에서 발생합니다.
*((_QWORD *)this + 10)
에는 이전에 SetPrintTicket
호출에서 할당된 힙 주소가 저장됩니다. 하지만 CWorkflowSession
COM 인터페이스는 MTA 모델에 속해있기 때문에 서로 다른 스레드에 의해 동시에 접근할 수 있습니다. 따라서 적절한 락 메커니즘이 존재하지 않으므로 레이스 컨디션을 통해 다음과 같은 형태로 Use-After-Free 혹은 Double Free가 발생할 수 있습니다.


몇 가지 제약 사항으로 인해 해당 취약점은 성공적으로 익스플로잇 하지 못했으나, 몇 가지 아이디어를 사용하면 개념적으로 익스플로잇이 가능해 보입니다.
먼저 ASLR bypass 입니다. ASLR의 경우 공격자가 컨트롤 하는 프로세스와 상위 권한의 프로세스의 동일한 DLL은 같은 가상주소에 매핑 되기 때문에 적절한 수준의 주소 정보를 알아 낼 수 있습니다. 이러한 아이디어는 IPC 메커니즘을 통한 LPE, SBX Escape에서 가장 기본이 되는 아이디어입니다.

두 번째로는 힙 주소 유출입니다.CWorkflowSession
에는 다른 메소드가 노출되어있고 이중 this + 10
주소에 접근하는 다른 메소드가 있는지 확인해 보았습니다. 그리고 마침내 다음과 같은 함수를 찾았습니다.

즉 이미 해제된 this + 10
에 있는 값을 Output Buffer에 완전히 안정적인 크기로 복사해주는 것을 알 수 있습니다. 즉 다음과 같은 시나리오로 유용한 힙 주소를 가져 올 수 있습니다.
먼저 총 세 개의 스레드가 필요합니다. 각각 그림과 같이 COM Interface를 호출하게 됩니다. 스레드 2에서 만약 this + 10
에 값이 존재하면 SetPrintTicket
에서 이미 존재하는 힙을 해제하게 됩니다. 이후 스레드 3에서 정상 할당이 진행되기 전에 vftable을 가지거나 힙 주소가 존재하는 객체를 할당하게 된다면 스레드 1에서 GetPrintTiket
을 통해 this + 10
주소를 참조하기 때문에 Victim 객체의 값을 성공적으로 공격자에게 리턴 할 수 있습니다. 공격자는 힙이 할당되는 사이즈 필드를 SetPrintTicket
에서 완전히 제어할 수 있기 때문에 Victim 객체의 크기 제약이 없어, Race Windows 내부에서 해제된 hole에 넣는 것은 매우 확정적으로 수행됩니다.

다음으로 RIP control은 어떻게 수행될 수 있을까요? 앞에서 설명한 힙 주소 유출 방법과 비슷한 방식으로 Corrupted된 vftable을 만들어 낼 수 있습니다.
총 세 개의 스레드가 사용됩니다. 앞선 방식과는 다르게 두 개의 스레드는 SetPrintTicket
을 통해 레이스를 시작합니다. 먼저 this + 10
이 null이 아니라면 힙을 해제하게 됩니다. 이 사이에 임의의 객체를 할당하여 해당 Hole에 들어가게 합니다. 다음으로 공격자가 완전히 컨트롤 가능한 값과 사이즈로 해당 Victim 객체를 덮을 수 있습니다. 해당 객체에 vftable이 존재한다면 성공적으로 RIP를 조작할 수 있게 됩니다.

이러한 아이디어에도 불구하고 우리는 완전히 동작하는 익스플로잇을 개발하지 못했습니다. 우리가 마주한 몇 가지 문제는 다음과 같습니다.
만약 Race Condition을 통해 해제된 위치에 Victim 객체를 할당하고 성공적으로 덮어썼다고 가정하여도 Double Free로 인해 발생하는 메모리 충돌을 피하기 매우 힘들고, Race Window가 매우 작아 정밀한 컨트롤이 매우 힘들었습니다.

4.1.4 CVE-2025-21234: Improper Input Validation in PrintWorkflowUserSvc
이제 마지막 취약점을 소개하고자 합니다. 이전 Case와 마찬가지로 PrintWorkflowUserSvc에서 발생합니다. IPrintSupportSourceSession
클래스의 RequestSpoolingHandlesForWrite
메소드를 살펴보겠습니다. 해당 메소드는 a2부터 a7까지 총 6개의 인자를 받으며, 모든 인자가 결과 값이 반환되는 Output Parameter로 사용됩니다. 여러분들은 버그를 찾을 때 Input Parameter가 하나도 존재하지 않는 함수를 분석하시나요? 우리의 대답 또한 아니오였습니다. 물론, 이 취약점을 찾기 전까지는 말입니다.

이 함수는 [1]에서 this
에 저장되어 있는, WorkflowSessionCommon
의 vftable을 가리키는 주소를 v9
에 저장합니다. 그리고, 해당 주소를 인자로 하여, [2]에서 WorkflowSessionCommon
클래스의 RequestSpoolingHandlesForWrite
메소드를 호출합니다.

이 메소드에서는 [1]에서 클라이언트의 PID를 가져와 [2]에서 클라이언트 프로세스의 핸들을 얻습니다. 이후, [3]에서 v22
에 this
를 참조하여 어떤 값을 저장하고, [4]에서 DuplicateHandle
함수를 호출합니다. 어라? DuplicateHandle
함수요?

DuplicateHandle
함수는, 객체의 핸들을 복제하는 Windows API 입니다. 함수 인자에 대한 설명을 요약하자면, 첫 번째와 두 번째 인자에 복제할 핸들을 소유한 프로세스 핸들과, 복제할 핸들이 들어갑니다. 그리고 세 번째에는 복제할 핸들을 받는 프로세스의 핸들이 들어가며 네 번째 인자에는 복제한 핸들의 값이 반환 됩니다. MSDN 설명에 의하면, PrintWorkflowUserSvc의 v22
핸들을, 클라이언트 프로세스의 복사하는 것을 뜻합니다. COM에서 이러한 패턴이 가끔 발견되기는 하지만, 보통은 콜백이나 메소드 구현을 위해 이벤트 핸들을 클라이언트 프로세스에 반환하고는 합니다.

v22
값이 실제로 어디서 세팅되는것은 간단한 리버스 엔지니어링을 통해 확인 할 수 있었습니다.
해당 값은 PrintSupportSession
클래스의 생성자에서 초기화되는 것으로 확인됐습니다. v22
는 -1
로 초기화됩니다. Windows에서 -1
이라는 핸들은 자기 자신을 가리키는 의사 핸들 값을 뜻합니다.

즉, DuplicateHandle
함수에서 자신의 프로세스 핸들을, 클라이언트 프로세스에게 복제해주는 것을 의미합니다. 그렇다는 것은, PrintWorkflowUserSvc 보다 낮은 권한을 가진 클라이언트가 이 메소드를 호출하면, PrintWorkflowUserSvc의 핸들을 자신의 프로세스로 복제할 수 있습니다.
어떤 프로세스가 PrintWorkflowUserSvc에 접근할 수 있는지, 이 서비스의 Access Permission을 확인해 봤습니다. 확인해본 결과, 이 COM 객체는 모든 UWP 앱과, 특정 Capability를 가진 Low Privileged AppContainer에서도 접근할 수 있다는 것을 확인했습니다. 이 뜻은 Sandox된 프로세스에서 PrintWorkflowUserSvc의 프로세스 핸들을 자신에게 복제할 수 있음을 의미합니다.

이 취약점은 정말 완벽한 로직버그 입니다. 권한 상승을 위해 전에 설명했던 복잡한 Use-After-Free 취약점을 익스플로잇할 필요가 전혀 없습니다. 이제 우리는, 복제된 핸들을 이용해 PrintWorkflowUserSvc 메모리에 쉘코드를 작성하고, 이를 CreateRemoteThread API를 이용해 실행시킬 것입니다.

4.4.3 Exploit Demo
취약점을 시연하기 위해 Adobe Acrobat 샌드박스 내에서 실행되는 렌더러에서 임의 코드 실행을 수행할 수 있다고 가정하고, 쉘코드를 주입하여 Sandbox Escape를 수행했습니다. 이를 통해 AppContainer에서 Medium으로의 권한 상승에 성공하였습니다.
놀랍게도 이 버그는, MSRC로부터 “Exploit Less Likely” 판정을 받았습니다. 우리는 아직까지 MSRC가 왜 이런 판단을 내렸는지 잘 모르겠습니다.

5. Results
마지막으로 결론입니다.
최종적으로 우리는 2024년부터 최근까지 Type Confusion과 Race Condition 취약점을 포함해 10건 이상의 취약점을 Microsoft에 제보했고, CVE로 인정받을 수 있었습니다.

이러한 성과를 낼 수 있었던 것은 선행 연구자들의 정보 공유와, oleviewdotnet이라는 매우 훌륭한 도구 덕분이었습니다. 특히 COM 취약점 분석을 시작할 수 있게 기반을 마련해주신, 선행 연구자들과 James Forshaw에게 깊은 감사의 말씀을 드립니다.

마지막으로 이번 여정을 마치면서 들었던 생각을 공유하고 마무리하겠습니다.
새로운 분야에서 새로운 취약점을 발견하기 위해서는 선행 연구에 대한 철저한 분석이 필수적이라고 생각합니다. 단순히 기존 연구를 따라가는 데 그치지 않고, 그 안에서 미처 다루지 못한 부분이나 확장 가능한 가능성에 주목하는 것이 중요하다는 점을 말씀드리고 싶습니다.
아쉬운 점도 있었습니다. 특히 여러 개의 Race Condition 취약점을 발견했음에도, 완전한 익스플로잇까지는 이뤄내지 못한 점은 앞으로 우리가 더 보완해야 할 부분이라고 생각합니다.
우리의 Future Work는, 이러한 취약점 패턴을 보다 체계적으로 탐지할 수 있는 도구를 개발하고, 이를 통해 더 많은 취약점을 찾아내는 것입니다. 우리의 여정은 앞으로도 계속될 것입니다.