1990년대 초부터 Microsoft Windows 개발의 초석이 되어온 COM(구성 요소 개체 모델)은 여전히 최신 Windows 운영 체제와 애플리케이션에서 널리 사용되고 있습니다. COM 구성 요소에 대한 의존도와 수년간의 광범위한 기능 개발로 인해 공격 표면이 넓어졌습니다. 2025년 2월, Google Project Zero의 James Forshaw(@tiraniddo)는 서버 측 DCOM 프로세스의 맥락에서 .NET 관리 코드를 실행하는 데 갇힌 COM 개체를 사용할 수 있는 분산 COM(DCOM) 원격 기술을 악용하는 새로운 방법을 자세히 설명하는 블로그 게시물을 발표했습니다. Forshaw는 권한 확대 및 PPL(Protected Process Light) 우회에 대한 여러 가지 사용 사례를 강조합니다.
Forshaw의 연구를 바탕으로 Mohamed Fakroud(@T3nb3w)는 2025년 3월 초에 PPL 보호를 우회하는 기술 구현을 발표했습니다. Jimmy Bayne(@bohops)와 저는 2025년 2월에 연구를 수행했으며, 그 결과 갇힌 COM 개체를 악용하여 개념 증명 파일리스 수평 이동 기술을 개발할 수 있었습니다.
COM은 기본 프로그래밍 언어에 관계없이 고유한 모듈식 구성 요소를 서로 상호 작용하고 애플리케이션과 상호 작용할 수 있도록 하는 바이너리 인터페이스 표준 및 미들웨어 서비스 계층입니다. 예를 들어 C++로 개발된 COM 개체는.NET 응용 프로그램과 쉽게 인터페이스할 수 있으므로 개발자는 다양한 소프트웨어 모듈을 효과적으로 통합할 수 있습니다. DCOM은 COM 클라이언트가 프로세스 간 통신(IPC) 또는 원격 프로시저 호출(RPC)을 통해 COM 서버와 통신할 수 있도록 하는 원격 기술입니다. 많은 Windows 서비스는 로컬 또는 원격으로 액세스할 수 있는 DCOM 구성 요소를 구현합니다.
COM 클래스는 일반적으로 Windows 레지스트리 내에 등록되고 포함됩니다. 클라이언트 프로그램은 COM 개체라고 하는 COM 클래스의 인스턴스를 만들어 COM 서버와 상호 작용합니다. 이 개체는 표준화된 인터페이스에 대한 포인터를 제공합니다. 클라이언트는 이 포인터를 사용하여 개체의 메서드와 속성에 액세스하여 클라이언트와 서버 간의 통신 및 기능을 용이하게 합니다.
COM 개체는 취약성 노출을 평가하고 악용 가능한 기능을 발견하기 위한 연구 대상이 되는 경우가 많습니다. 갇힌 COM 개체는 COM 클라이언트가 프로세스 외부 DCOM 서버에서 COM 클래스를 인스턴스화하고, 클라이언트가 참조로 마샬링된 개체 포인터를 통해 COM 개체를 제어하는 버그 클래스입니다. 조건에 따라 이 제어 벡터는 보안 관련 논리 결함이 있을 수 있습니다.
Forshaw의 블로그에서는 WaaSRemediation COM 클래스에 노출된 IDispatch 인터페이스가 갇힌 COM 개체 남용 및 .NET 코드 실행을 위해 조작되는 PPL 우회 사용 사례를 설명합니다. WaaSRemediation은 NT AUTHORITY\SYSTEM의 컨텍스트에서 보호된 svchost.exe 프로세스로 실행되는 WaaSMedicSvc 서비스에서 구현됩니다. Forshaw의 뛰어난 워크스루는 개념 증명용 파일리스 수평 이동 기법의 응용 연구 및 개발의 기반이 되었습니다.
저희의 연구 여정은 IDispatch 인터페이스를 지원하는 WaaSRemediation COM 클래스를 살펴보는 것으로 시작되었습니다. 이 인터페이스를 통해 클라이언트는 후기 바인딩을 수행할 수 있습니다. 일반적으로 COM 클라이언트는 컴파일 시 사용 중인 개체에 대한 인터페이스 및 유형 정의가 정의되어 있습니다. 대신 지연 바인딩을 사용하면 클라이언트가 런타임에 개체에 대한 메서드를 검색하고 호출할 수 있습니다. IDispatch에는 ITypeInfo 인터페이스를 반환하는 GetTypeInfo 메서드가 포함되어 있습니다. ITypeInfo에는 이를 구현하는 개체에 대한 형식 정보를 검색하는 데 사용할 수 있는 메서드가 있습니다.
COM 클래스가 형식 라이브러리를 사용하는 경우 클라이언트는 ITYPELib(ITypeInfo-> GetContainingTypeLib에서 가져옴)를 통해 쿼리하여 형식 정보를 검색할 수 있습니다. 또한 유형 라이브러리는 추가 유형 정보를 위해 다른 유형 라이브러리를 참조할 수도 있습니다.
Forshaw의 블로그 게시물에 따르면 WaaSRemediation은 WaaSRemediationLib 형식 라이브러리를 참조하며, 이는 stdole(OLE 자동화)을 참조합니다. WaaSRemediationLib은 해당 라이브러리의 두 가지 COM 클래스인 StdFont와 StdPicture를 활용합니다. TreatAs 레지스트리 키를 수정하여 StdFont 개체에 대해 COM Hijacking을 수행하면 클래스가 .NET Framework의 System.Object와 같이 선택한 다른 COM 클래스를 가리킵니다. Forshaw는 StdPicture가 아웃 오브 프로세스 인스턴스화 검사를 수행하기 때문에 StdPicture가 실행 가능하지 않다고 지적했기 때문에 StdFont 사용에 계속 초점을 맞췄습니다.
.NET 개체는 System.Object의 GetType 메서드 때문에 흥미롭습니다. GetType을 통해 .NET 리플렉션을 수행하여 결국 Assembly.Load에 액세스할 수 있습니다. System.Object가 선택되었지만 이 유형은 .NET에서 유형 계층 구조의 루트입니다. 따라서 모든 .NET COM 개체를 사용할 수 있습니다.
초기 단계가 설정되면 HKLM\Software\Microsoft\.NetFramework 키 아래에 두 개의 다른 DWORD 값이 필요하여 저희가 인식한 사용 사례를 실현할 수 있었습니다.
초기 테스트 작업에서 최신 버전의 CLR 및 .NET을 로드할 수 있음을 확인하자마자 올바른 방향으로 가고 있다는 것을 알 수 있었습니다.
원격 프로그래밍 측면에 초점을 맞추기 위해 먼저 원격 레지스트리를 사용하여 .NetFramework 레지스트리 키 값을 조작하고 대상 컴퓨터에서 StdFont 개체를 하이재킹했습니다. 다음으로, 원격 대상에서 WaaSRemediation COM 개체를 인스턴스화하고 IDispatch 인터페이스에 대한 포인터를 가져오기 위해 CoCreateInstance를 CoCreateInstanceEx로 교체했습니다.
IDispatch에 대한 포인터를 사용하여 GetTypeInfo 멤버 메서드를 호출하여 서버에 갇혀 있는 ITypeInfo 인터페이스에 대한 포인터를 가져옵니다. 이후 호출된 멤버 메서드는 서버 측에서 발생합니다. 관심 있는 포함된 형식 라이브러리 참조(stdole)를 식별하고 관심 있는 후속 클래스 개체 참조(StdFont)를 도출한 후, 결국 ItypeInfo 인터페이스에서 "remotable" CreateInstance 메서드를 사용하여 StdFont 개체 링크 흐름을 리디렉션하여(이전 TreatAs 조작을 통해) System.Object를 인스턴스화했습니다.
AllowDCOMReflection이 제대로 설정되었으므로 DCOM을 통해 .NET 리플렉션을 수행하여 Assembly.Load에 액세스하여 .NET 어셈블리를 COM 서버에 로드할 수 있습니다. DCOM을 통해 Assembly.Load를 사용하고 있기 때문에 어셈블리 바이트 전송이 DCOM 원격 마법에 의해 처리되므로 이 수평 이동 기술은 완전히 파일리스입니다. 개체 인스턴스화에서 반사까지의 기술적 흐름에 대한 자세한 설명은 다음 다이어그램을 참조하세요.
첫 번째이자 주요 문제는 IDispatch->Invoke를 통해 Assembly.Load_3을 호출하는 것이었습니다. Invoke는 인수의 개체 배열을 대상 함수에 전달하고, Load_3은 단일 바이트 배열을 사용하는 Assembly.Load의 오버로드입니다. 따라서 바이트의 SAFEARRAY를 VARIANT의 또 다른 SAFEARRAY 내에 래핑해야 했습니다. 처음에는 바이트로 구성된 단일 SAFEARRAY를 계속 전달하려고 했습니다.
또 다른 문제는 적절한 Assembly.Load 과부하를 찾는 것이었습니다. 헬퍼 함수는 GetStaticMethod 함수가 포함된 Forshaw의 CVE-2014-0257 코드에서 가져왔습니다. 이 함수는 DCOM을 통한 .NET 리플렉션을 사용하여 형식 포인터, 메서드 이름 및 매개 변수 수가 주어진 정적 메서드를 찾았습니다. Assembly.Load에는 단일 인수를 사용하는 두 개의 정적 오버로드가 있습니다. 결국 저희는 해키 솔루션을 사용하게 되었습니다. 저희는 단일 인수를 가진 Load의 세 번째 인스턴스가 우리의 올바른 선택이라는 것을 알았습니다.
이 기술에서 저희가 관찰한 가장 큰 단점 중 하나는 생성된 비콘의 수명이 COM 클라이언트로 제한된다는 것입니다. 이 경우 무기화 바이너리 "ForsHops.exe"의 애플리케이션 수명은 다음과 같습니다. (물론 이름도 우아하게 지었습니다). 따라서 ForsHops.exe가 COM 참조를 정리하거나 종료하면 원격 시스템의 svchost.exe에서 실행 중인 비콘도 정리됩니다. .NET 어셈블리가 메인 스레드를 무기한 중단하고, 다른 스레드에서 셸코드를 실행하고, ForsHops.exe가 익스플로잇 스레드를 중단된 상태로 두는 등 다양한 솔루션을 시도했지만, 아무 소용이 없었습니다.
현재 상태에서 ForsHops.exe는 비콘이 종료될 때까지 실행되며, 그 시점이 지나면 레지스트리 작업을 제거합니다. 개선의 여지가 있지만 독자를 위한 연습 문제로 남겨 두겠습니다.
Mohamed Fakroud가 구현을 발표한 후 Samir Bousseaden(@SBousseaden)이 제안한 탐지 지침도 이 수평 이동 기술에 적용됩니다.
또한 다음과 같은 추가 제어 기능을 구현하는 것이 좋습니다.
또한 다음 개념 증명 YARA 규칙을 활용하여 표준 ForsHops.exe 실행 파일을 탐지합니다.
rule Detect_Standard_ForsHops_PE_By_Hash
저희의 구현은 Forshaw의 블로그에서 설명한 COM 남용을 약간 확장하여 PPL 우회를 위해 로컬 실행이 아닌 수평 이동을 위해 갇힌 COM 개체를 활용합니다. 따라서 로컬 실행을 수행하는 구현과 동일한 탐지에 여전히 취약합니다.
ForsHops.exe 개념 증명 수평 이동 코드는 여기에서 찾을 수 있습니다.
이 연구에 대한 피드백을 제공하고 블로그 게시물을 검토해 주신 Dwight Hohnstein(@djhohnstein)과 Sanjiv Kawa(@sanjivkawa)에게 감사합니다.