
CVE-2024-30051에 대한 상세 기술 분석 및 개념 증명(PoC) 익스플로잇으로, Windows DWM Core Library의 힙 기반 버퍼 오버플로우를 악용하여 무결성 시스템(Integrity System) 수준으로의 로컬 권한 상승을 가능하게 합니다.
이 블로그 게시물에서는 Core Impact용 익스플로잇을 개발할 당시 분석했던 Microsoft Windows DWM Core 라이브러리의 취약점에 대해 설명합니다. 이 취약점은 권한이 없는 공격자가 Integrity System 권한을 가진 DWM 사용자로 코드를 실행할 수 있게 합니다 (CVE-2024-30051).
당시 익스플로잇을 개발하기에 충분한 공개 정보가 없어서 리버싱을 많이 해야 했습니다. 그래서 여기서는 IDA PRO를 사용하여 Windows 23H2용 KB5037771 패치를 리버싱하는 방법을 보여드리고, BINDIFF를 사용하여 dwmcore.dll 버전 10.0.22621.3447과 버전 10.0.22621.3593 간의 바이너리 디핑(binary diffing)을 수행하는 방법을 설명하겠습니다. 또한 힙 오버플로가 어떻게 발생하는지 보여주고, 권한을 상승시켜 이를 익스플로잇한 다음 마지막으로 작동하는 PoC를 만들겠습니다.
색인:
[Windows DWM Core Library 권한 상승 취약점 (CVE-2024-30051) 1](#windows-dwm-core-library-elevation-of-privilege-vulnerability-cve-2024-30051)
[취약점 세부 정보: 2](#vulnerability-details)
[버그를 찾기 위한 Diffing: 3](#diffing-to-find-the-bug)
[CVE-2024-30051을 익스플로잇하는 PoC 분석: 8](#analysis-of-the-poc-exploiting-cve-2024-30051)
[1) 초기화 8](#initialization)
[2) 후킹 8](#hooking)
[3) 창 생성 16](#creating-the-window)
[4) 디바이스 생성 16](#create-device)
[5) 팩토리 생성 22](#create-factory)
[6) 디바이스 컨텍스트 생성 28](#create-a-device-context)
[7) 컴포지션 디바이스 생성 29](#create-a-composition-device)
[8) hook3 함수 호출 31](#calling-dcompositioncreatedevice-function)
[9) HWND 대상 생성 32](#creating-a-target-for-handle-hwnd)
[10) Surface 생성 33](#creating-surface)
[11) BeginDraw, EndDraw, CreateVisual 호출 34](#calling-begindraw-enddraw-and-createvisual)
[11) Visual SetContent 호출 36](#calling-visual-setcontent)
[12) 개체 해제 38](#release-objects)
[13) 컴포지션 디바이스 커밋 38](#commit-composition-device)
[14) hook2 호출 39](#calling-hook2)
[15) hook 호출 39](#remember-that-the-vulnerable-function-can-be-reached-using-some-methods-of-the-cprimitivegroup-class.-at-this-point-it-creates-a-heap-then-hook2-captures-and-saves-the-corresponding-heaphandle.)
[16) hook4 호출 41](#calling-the-function-hook4)
[17) 힙 스프레이 수행 49](#performing-heap-spray)
[18) 전송 전 base chunk 수정 51](#modifying-the-base-chunk-before-send)
[19) DWM 프로세스 디버깅 52](#debugging-the-dwm-process)
[20) 권한을 Integrity System 수준으로 상승 62](#elevating-privileges-to-integrity-system-level)
Windows DWM Core Library 권한 상승 취약점 CVE-2024-30051
출시: 2024년 5월 14일
CNA 지정: Microsoft CVE-2024-30051
영향: 권한 상승
최대 심각도: 중요
약점:
CWE-122: 힙 기반 버퍼 오버플로
CVSS: 3.1 7.8 / 7.2
이 취약점은 dwmcore.dll이라고 하는 Windows DWM 메인 라이브러리 내 정수 나눗셈의 크기 계산 오류로 인해 발생합니다. 로컬 사용자는 dwmcore.dll의 CCommandBuffer::Initialize 메서드에서 힙 버퍼 오버플로를 유발하고 Integrity System 권한을 가진 DWM 사용자로 임의 코드를 실행할 수 있습니다. 익스플로잇은 DWM 프로세스에서 힙 스프레이(Heap Spray)를 수행하여 메모리를 준비하고, 최종적으로 dwmcore.dll에 힙 오버플로를 발생시킵니다. 이 힙 오버플로는 힙 스프레이의 특정 부분을 해제할 때 트리거됩니다.
익스플로잇이 성공하면 DWM 프로세스는 우리가 만든 DLL을 로드하여, Integrity System 권한을 가진 DWM 사용자로 우리의 코드 또는 실행 파일(이 경우 CMD)을 실행합니다.

이 취약점을 살펴보면서 어떻게 Integrity Level SYSTEM을 가진 DWM 사용자로 실행할 수 있게 되는지 알아보겠습니다. 이 사용자는 Administrator 그룹에 속하지 않으므로 일부 권한 제한이 있습니다.
Windows 11 23H2용 패치는 다음에서 다운로드할 수 있습니다:
https://www.catalog.update.microsoft.com/Search.aspx?q=KB5037771
windows11.0-kb5037771-x64_19a3f100fb8437d059d7ee2b879fe8e48a1bae42.msu
취약한 버전의 dwmcore.dll: 10.0.22621.3447
패치된 버전의 dwmcore.dll: 10.0.22621.3593
변경된 함수들을 분석해 보면, 패치된 CCommandBuffer::Initialize 버전에는 블록이 많이 추가되어 패치 전 버전과 상당히 다르게 보인다는 것을 알 수 있습니다.

해당 함수를 정적 리버싱한 결과, CD2DSharedBuffer::GetBufferSize에 대한 호출이 두 번 있습니다.
첫 번째 호출은 new에서 할당할 크기(size) 를 가져오고, 두 번째 호출은 memcpy를 위해 동일한 크기를 가져옵니다.

처음에는 모든 것이 올바른 것처럼 보입니다. 그러나 할당 전에 크기로 몇 가지 연산을 수행합니다.

동일한 CD2DSharedBuffer::GetBufferSize 함수를 호출하여 buffer_size와 buffer_size2를 얻으며, 둘 다 같은 값을 반환합니다. 그러나 new에서는 사전 연산으로 buffer_size를 0x90으로 정수 나눗셈한 다음 0x90을 곱하는 반면, memcpy에서는 반환된 buffer_size2를 연산 없이 사용합니다.
이러한 연산을 통해 new에서 최종 사용되는 크기와 memcpy에서 사용되는 크기가 달라질 수 있음을 발견했습니다.
buffer_size = buffer_size2 (반환된 크기)
size_new= buffer_size/0x90 x 0x90 =0x90
size_memcpy= buffer_size2= 0x91
예를 들어 buffer_size가 0x91인 경우
buffer_size = buffer_size2=0x91
size_new= buffer_size/0x90 x 0x90 =0x90
size_memcpy= buffer_size2= 0x91
이 예는 힙 오버플로가 발생함을 입증합니다. 할당된 바이트보다 더 많은 바이트를 복사하며, 그 크기는 제어할 수 있습니다.
예를 들어 POC에서 사용한 것처럼 buffer_size가 0x23f인 경우입니다.
buffer_size = buffer_size2=0x23F
size_new= buffer_size/0x90 x 0x90 =0x1b0
size_memcpy== buffer_size2=0x23f
취약한 함수를 분석한 후, 취약한 함수 CCommandBuffer::Initialize에 도달하는 방법을 확인하고 싶었습니다. 여기서부터 상황이 복잡해지기 시작합니다.
이 함수에 대한 참조를 다시 살펴보면, CPrimitiveGroup 클래스의 메서드에서 도달하는 것처럼 보입니다:

이러한 메서드는 CPrimitiveGroup 개체의 vftable에서 접근할 수 있습니다:
이 클래스에는 생성자가 있습니다:

그리고 다음과 같은 방식으로 도달합니다:

처음 이 과정을 진행하면서 PDF "The Lost World of DirectComposition: Hunting Windows Desktop Window Manager Bugs"를 읽는 시간을 가졌고 Direct Composition의 세계에 깊이 들어가 보았습니다. 이것은 제 첫 PoC를 만드는 데 도움이 되었습니다.
또한 win32ksys를 리버싱해야 했고 다음 함수들을 통해 패키지를 보내려고 시도했습니다:
NtDCompositionCreateChannel
NtDCompositionProcessChannelBatchBuffer
NtDCompositionCommitChannel

첫 번째 PoC는 CPrimitiveGroup 생성자에 도달했습니다. 그러나 많은 리버싱 끝에도 이 함수들을 사용한 ALPC 호출을 통해 vftable 메서드 호출을 처리하여 취약한 함수에 직접 도달하는 방법을 찾지 못했습니다.
복잡한 리버싱 작업에 많은 시간을 보냈습니다. 그 과정에서 이 취약점을 익스플로잇한 악성코드 샘플을 발견했는데, 익스플로잇 방법이 처음 생각했던 것보다 훨씬 복잡했기 때문에 매우 도움이 되었습니다. 또한 시스템 API에 대한 여러 후킹이 포함되어 있으며 다소 논란이 될 수 있는 방법들을 사용합니다. 하지만 전쟁과 익스플로잇에서는 무엇이든 유효한 법이므로, 악성코드를 분석하기 시작했고 그 분석을 바탕으로 마침내 취약점을 익스플로잇하는 최종 PoC를 만들었습니다. 이에 대해서는 아래에서 설명하겠습니다.
먼저 분명히 하고 싶은 것은, 이 악성코드는 프로세스를 Integrity System Level로 상승시키는 CVE-2024-30051 취약점을 익스플로잇할 뿐만 아니라, 두 번째 단계를 수행하여 거기서부터 모든 권한을 가진 SYSTEM 사용자로까지 상승시킨다는 점입니다. 이는 설명된 CVE의 범위를 이미 넘어섭니다.
또한, 악성코드는 코드를 최소화하려는 제 PoC보다 훨씬 복잡하다는 점을 알아두는 것이 중요합니다. 악성코드는 안정성을 보장하기 위해 훨씬 더 많은 검사를 수행하며, 그 덕분에 첫 시도에 작동합니다. 저는 단순화를 위해 그러한 검사를 모두 버리고 순수한 익스플로잇에만 집중했습니다. 그래서 익스플로잇에 성공하려면 PoC를 두세 번 실행해야 할 수도 있습니다.
실행 가능한 PoC의 링크는 다음과 같습니다: https://github.com/fortra/CVE-2024-30051
먼저 PoC는 GetVersion을 호출하여 실행 중인 OS 버전을 확인하고, 그에 따라 일부 전역 변수를 다르게 초기화합니다. 제 PoC는 Windows 11 23H2와 Windows 11 22H2에서 테스트되었습니다. 다른 시스템도 취약하며, 이를 익스플로잇하기 위한 값들이 추가되어 있습니다.
시스템 함수 4개를 후킹하며, 이 함수들을 후킹하지 않으면 익스플로잇을 달성할 수 없습니다. 그 함수들은 다음과 같습니다: RtlAllocateHeap, RtlCreateHeap, NtDCompositionCreateChannel 및 NtDCompositionCommitChannel.

이 함수들에서 처음 5바이트를 패치하여 자체 코드로 점프하게 만듭니다. 물론 5바이트 점프는 전체 메모리를 커버하지 못하므로 코드는 멀리 떨어질 수 없고 가까이 있어야 합니다.
이를 위해 악성코드는 매우 긴 코드를 사용하여 메모리 맵을 분석한 후 자체 코드를 할당할 위치를 결정합니다. 코드가 복잡해서 저는 두 줄의 간단한 코드로 만들었습니다:
base_ntdll = GetModuleHandleW(L"ntdll.dll");
global4_ = (char *)VirtualAlloc((LPVOID)(base_ntdll-0x2000), 0x1000uLL, 0x3000u, 0x40u);
ntdll 베이스에서 0x2000을 뺀 후 그 주소를 VirtualAlloc에 전달하여 거기에 할당했습니다.
64비트 DLL은 메모리에서 서로 상당히 분리되어 매핑되며, 그 사이에 빈 공간이 있습니다.
후크(hook) 가 어떻게 작동하는지 살펴보겠습니다:

인자가 세 개인 후킹 함수를 호출하는데, 이 함수가 RtlAllocateHeap API의 후킹을 수행합니다. 첫 번째 인자는 패치할 API의 주소로 sym_RtlAllocateHeap이라고 합니다.
패치 전에는 API의 시작 부분을 가리킵니다:

여기에 RtlAllocateHeap 함수가 있습니다:

두 번째 인자는 hook이라고 하는 루틴으로, API가 완전히 패치된 후 실행됩니다:


hook 함수는 my_RtlAllocateHeap을 호출합니다.
hooking 함수는 API의 처음 5바이트를 패치하여 hook으로 점프하게 만듭니다.
할당된 영역의 코드를 호출하여 5바이트로 대체된 API의 첫 번째 명령을 실행한 다음, 패치된 바이트 바로 뒤인 RtlAllocateHeap+5로 점프합니다:

이것이 후킹 후 API의 모습입니다. hook으로 점프하도록 처음 5바이트가 변경되었습니다. 바로 위에 있는 코드인 my_RtlAllocateHeap을 호출하며, 이 코드는 보라색으로 표시된 영역으로 돌아가 API 실행을 계속합니다:

API 실행이 끝나면 hook으로 돌아갑니다. 거기서 전역 변수 heap_base(초기값은 0)를 RtlAllocateHeap에 전달된 첫 번째 인자와 비교합니다:

그 후 코드는 특정 HeapHandle을 가진 특별한 할당을 기다립니다. 처음에는 이 변수가 0이며, 0인 동안에는 건너뛰고 일반적인 RtlAllocateheap처럼 동작합니다:

HeapHandle 매개변수는 공교롭게도 두 번째로 후킹된 API인 RtlCreateHeap 내부에서 얻습니다.
전역 변수 heap_base에 대한 참조를 찾아보면, 그 값은 RtlCreateHeap 후킹 후 실행되는 hook2 함수에서만 변경됩니다:


그래서 아이디어는 특정 HeapHandle을 캡처하여 heap_base에 저장하는 것입니다. 이제 0이 아니므로 hook 함수는 각 할당을 비교하기 시작합니다. 따라서 PoC는 이전에 저장된 것과 동일한 HeapHandle을 가진 메모리 주소를 저장합니다.
그런 경우 할당의 주소를 base라는 변수에 저장합니다:
이제 처음 두 후크는 서로 연결됩니다. hook2가 기대하는 HeapHandle 값을 저장하면, 동일한 HeapHandle을 사용하는 할당 주소를 저장하는 hook 함수가 활성화됩니다.
세 번째 후크는 NtDCompositionCreateChannel에 적용됩니다. 처음 호출될 때 세 번째 인자의 내용인 MappedAddress를 저장합니다. 그런 다음 hooked_flag를 1로 변경하여 이후로는 더 이상 저장하지 않고 정상적으로 동작합니다.


변수 base에 저장된 주소는 나중에 세 번 읽힙니다. 그중 두 번은 마지막 후크인 hook4에서 발생합니다:

NtDCompositionCommitChannel에 대한 hook4 함수는 상당히 복잡하고 매우 중요하므로 나중에 분석하겠습니다.
네 개의 후크가 완료되면 메인 함수로 돌아와 창 생성을 시작합니다. 이는 RegisterClassExW를 호출하여 수행됩니다. 그러나 나중에 사용할 창 클래스를 등록한 후에는 CreateWindowExW 함수로 창을 생성합니다.

이는 CoInitializeEx를 호출하여 호출 스레드에서 사용할 COM 라이브러리를 초기화합니다:

원하는 크기를 기준으로 창 사각형에 필요한 크기를 계산합니다:

그려질 창을 만들기 위해 CreateWindowExW 함수가 호출됩니다:

그런 다음 D3D11CreateDevice를 호출하여 디스플레이 어댑터를 나타내는 디바이스 또는 DirectX 디바이스를 생성합니다:


제 PoC에서 ppDevice는 d3dDevice로, ppInmediateContext는 d3dContext로 이름 지었습니다:

flags 인자는 0x20으로 설정해야 합니다:

그런 다음 AddRef를 호출합니다:

이는 COM 개체에 대한 인터페이스 포인터의 참조 카운터를 증가시킵니다:


THIS에서 0x10 값이 차감됩니다:

ID3D11Device-0x10에서 오프셋 0xf8 위치에 TComObject에 대한 포인터가 있습니다:



이것이 새 THIS가 되며 결국 TComObject::AddRef로 점프하게 됩니다:

그리고 TComObject의 오프셋 8에 있는 개체 카운터에 1을 더하는 것으로 끝납니다:

그런 다음 AddRef는 D3D11CreateDevice에서 생성된 다른 개체 유형인 ID3D11DeviceContext의 카운터를 증가시킵니다:

이 경우 새 THIS를 찾기 위해 0x108을 차감합니다:


오프셋 0x98에 새 THIS가 있는 이 위치로 점프합니다:

이것이 카운터입니다. 이 예에서는 QWORD입니다:

PoC는 Direct2D를 사용하고, 도형을 그리거나 설명하는 데 사용할 수 있는 다른 Direct2D 리소스를 생성하는 데 사용되는 ID2D1Factory 인터페이스를 만들기 위해 D2D1CreateFactory를 호출합니다:

riid 인자는 Microsoft 페이지에서 제안하는 것입니다:
https://learn.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-d2d1createfactory

악성코드가 사용하는 것은 다음과 같습니다:

ID2D1Factory에 맞는 올바른 값은 여기에서 찾을 수 있습니다**:**
https://github.com/apitrace/dxsdk/blob/master/Include/d2d1_1.h

저는 Direct Composition 전문가가 아니므로 악성코드와 동일한 단계를 사용했습니다:
반환되는 새 팩토리는 구체적인 유형을 제공하지 않습니다. void *로 표시되는데, 이는 공식적으로 문서화되지 않았음을 의미합니다:

이 경우와 같은 개체 유형을 모르기 때문에, 메모리에서 쉽게 확인할 수 있도록 이를 사용하는 실행 파일을 개발했습니다:

네 개의 hook 함수에 중단점을 추가합니다. 이 경우 hook2의 중단점은 HeapHandle을 캡처하는 시점을 보여줍니다:

원하는 chunk가 캡처되면 hook이 중단되어야 합니다:

나머지 두 hook에도 중단점을 설정합니다:

그런 다음 QueryInterface 호출을 계속합니다:

https://help.solidworks.com/2020/english/api/sldworksapi/queryinterface_example_cplusplus_com.htm
https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/shared/dxgi.idl

일종의 동적 캐스팅(dynamic casting)을 수행하려고 시도합니다. ID3D11Device 유형의 개체가 IDXGIDevice의 인터페이스(메서드 사용 등)를 수용할 수 있으면, 새 유형을 수용하는 원본 개체의 복사본을 만든 다음 그에 대한 포인터를 반환합니다. 이 경우 변수 d3dContext1은 IDXGIDevice 유형이 됩니다:

두 개체 모두 **CLayeredObject<Cdevice>**에서 상속됩니다.
원본 ID3D11Device는 다음과 같습니다**:**

포인터를 반환하는 바로 그 개체입니다.

그런 다음 CreateDevice 함수로 ID2D1Device 개체를 생성합니다:

value2에는 ID2D1Device 유형의 개체가 반환됩니다.
https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext

여기서는 PoC에서 다음과 같이 구현됩니다:


그런 다음 DCompositionCreateDevice를 호출합니다
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatedevice


IID는 _IDCompositionDevice에 속합니다


DCompositionCreateDevice를 통해 함수를 추적하는 동시에, NtDCompositionCreateChannel을 호출할 때 hook3에서 멈춥니다:

이렇게 하여 DCompositionCreateDevice가 호출되었을 때 시스템이 내부적으로 사용하는 MappedAddress를 캡처합니다:
여기까지의 콜 스택은 다음과 같습니다:

dcomp 모듈이 NtDCompositionCreateChannel 함수를 호출하는 지점입니다:

이전 단계에서 돌아온 후 MappedAddress를 저장합니다. ALPC를 사용하여 DWM 프로세스에 연결한 다음 CreateTargetForHwnd를 호출합니다

생성된 창의 HWND 핸들을 사용합니다. 방금 생성한 디바이스와 관련이 있으며, 이 디바이스가 이 메서드의 THIS입니다:

그런 다음 CreateSurface를 호출합니다
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createsurface

그런 다음 BeginDraw, EndDraw를 호출하고 CreateVisual에 도달합니다.

BeginDraw를 호출합니다
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-begindraw
여기서는 IID _IDXGISurface를 사용합니다:


그런 다음 EndDraw를 사용합니다:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-enddraw


마지막으로 CreateVisual을 호출합니다:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createvisual

다음으로 IDCompositionVisual::SetContent를 호출합니다:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionvisual-setcontent


그리고 SetRoot를 호출합니다:

BeginDraw에서 수신되는 updateObject는 문서에서 유형이 명시되어 있지 않습니다.

다음으로, 이전에 생성된 객체들을 해제합니다:

이제 동일한 IDCompositionDevice 유형의 dcompDevice 객체를 사용하여 Commit 메서드를 호출합니다:


해당 Commit 메서드를 호출하면 원하는 HeapHandle을 캡처하는 hook2에서 멈춥니다:

지금의 콜 스택은 다음과 같습니다:

main으로 돌아가기 전에 RtlAllocateHeap을 사용하여 청크도 생성합니다. 그런 다음 hook 함수 내부의 base 변수에 캡처되어 저장됩니다:


Create 및 Allocate 호출은 차례로 수행됩니다:

(Allocate와 Create) 둘 다 DirectComposition::Cdevice::Commit에서 호출됩니다:

그 후 NtDCompositionCommitChannel이 호출되면 hook4에서 멈춥니다:

NtDCompositionCommitChannel은 여기에서 호출됩니다:

또한 DirectComposition::Cdevice::Commit에서도 호출됩니다

시스템이 이미 ALPC를 통해 DWM으로 보낼 명령을 배치했다는 점을 언급할 가치가 있습니다. 그 후 NtDCompositionCommitChannel을 사용하여 명령을 보냅니다.
hook4 함수는 NtDCompositionCommitChannel 호출을 가로채며, 이 시점에서 배치에 더 많은 명령이 추가됩니다.
hook4가 수행하는 작업을 살펴보겠습니다:
base가 가리키는 청크를 통해 루프가 수행됩니다.
청크 내에서 0x120 값을 찾으면 루프를 종료합니다:

0x120 값이 위치한 주소와 오프셋을 저장합니다:

0x120 값을 0x1b0 + 0x8f = 0x23f와 같은 value4로 덮어씁니다. 이것은 오버플로우 시 memcpy에서 사용할 크기입니다:



0x120이 위치한 주소 포인터에 0xbc + 0x90을 더합니다:


base에서 오프셋 0x48에 크기 0x120이 있었다는 것을 기억하세요. 그것은 0x23f로 덮어써졌으므로 원래 청크는 0x120 크기여야 합니다:
소스는 0x23f + 0x2c의 포인터 주소입니다:

초기에는 0x90을 더했지만 지금은 다시 0x90을 뺍니다.
대상은 0x120에 대한 포인터 + 0xbc의 주소가 됩니다:

다음 위치에 쓰게 됩니다:

모든 쓰기는 청크 내부에서 이루어집니다:

0x1b0/0x90을 정수로 나눈 결과인 3회 루프를 반복합니다:

그 후, ArgChannelHandle 채널은 MappedAddress가 캡처되었을 때 사용된 것과 동일한 채널이므로, PoC는 NtDCompositionProcessChannelBatchBuffer를 사용하여 배치에 명령을 추가합니다. 이 명령들은 시스템이 추가했던 명령들과 함께 처리됩니다. 배치가 이를 수집한 다음 NtDCompositionCommitChannel을 사용하여 명령이 모두 함께 전송됩니다:


전송된 명령은 값 8을 가지며, 이는 4개의 서로 다른 트래커(1,2,3,4)에 대한 SetResourceIntegerProperty에 해당합니다.
PoC가 main 함수로 돌아오면 HeapSpray를 수행하기 위해 다른 채널을 생성합니다.
0x10000개의 명령을 배치하며, _NtDCompositionCommitChannel로 전송됩니다:

여기서는 CreateResource=1 값과 CHolographicInteropTextureMarshaler = 0x50에 해당하는 유형을 사용합니다:

할당은 아래 코드에서 수행됩니다. 스프레이를 위해 생성된 객체의 크기는 0x1b0입니다:

그런 다음 이전 단계에서 생성된 객체를 해제하는 루프를 수행하여 메모리 분포에 구멍을 만듭니다.
counter2 변수는 0x3000에서 시작하여 0x7000보다 작은 동안 0x20씩 증가합니다:

base + 0x48 + 44 + 0x1b0에 있던 청크의 방향에서 0x41들을 씁니다.
즉, 나중에 인접한 청크를 오버플로우할 때 사용될 값들을 쓰고 있는 것입니다:
그 pvalue7은 base에서 0x224 주소에 위치합니다:

그런 다음 "escribe" 함수로 이동합니다:

pKernelCallbacktable에 0x388을 더한 값, LoadLibraryA 주소, 그리고 로드할 DLL의 경로를 씁니다. 이 경우 DLL 이름을 s11.dll로 지정했습니다.

이제 힙 오버플로우가 발생할 때 취약한 함수에서 멈추려면 커널 디버거가 필요합니다. DWM 프로세스는 사용자 모드 디버거로 디버깅할 수 없기 때문입니다.
IDA PRO를 사용하여 대상을 원격 디버깅할 때, 크기가 0x1b0과 같으면 멈추도록 조건부 중단점을 설정합니다:
print ("VALUE1 %x" % ((cpu.rax)))
return cpu.rax==0x1b0.
커널에서 사용자 모드 프로그램을 디버깅하고 있으므로 중단점을 설정하려면 DWM 프로세스 컨텍스트로 전환해야 합니다. 다음 명령으로 사용자 심볼을 다시 로드합니다:
. reload /user
다음 명령으로 커널 심볼을 다시 로드합니다:
. reload /f

ShowWindow를 스텝 오버하면 멈춥니다**:**

크기 0x1b0으로 할당하고 크기 0x23f로 복사하여 힙 오버플로우를 발생시킵니다:

이 시점의 콜 스택은 다음과 같습니다:
오버플로우를 만들기 위해 DWM은 아래 코드에서 값을 수신합니다:
PoC에서 보낸 base의 조작된 값들은 DWM 프로세스의 dwmcore.dll 모듈에서 MapViewofFile을 사용하여 읽습니다:

이전 함수는 다음에서 호출됩니다:


hook4에서 **destination_copy(NtDCompositionCommitChannel)**를 사용하여 ALPC로 보낼 때 멈춥니다:

hook4에서 배치에 명령이 추가되었음을 기억하세요. 그러나 시스템도 이미 base와 조작된 데이터를 포함한 일부 명령을 배치에 추가했습니다:



이 경우 000001cd'178d0000에서 시작하는 메모리 영역을 공유합니다. memcpy를 수행하기 위해 이를 소스로 사용하면 동일한 메모리 영역에서 0x794 바이트 뒤가 됩니다.
공유 메모리 영역의 크기는 0x4000입니다:

할당할 크기가 0x1b0일 때 멈추고, 0x23f 바이트를 복사하는 memcpy에 도달합니다:

메모리에서 0x1b0 너머에는 인접한 블록을 덮어쓰며 오버플로우할 코드가 있습니다:
PoC에서 청크가 해제되면 조작된 라이브러리를 로드하는 LoadLibraryA로 점프하면서 끝납니다:

그것은 여기에서 비롯됩니다:


힙 스프레이는 CHolographicInteropTexture 유형의 0x1b0 크기 객체로 수행되었습니다.
메모리 분포에 구멍을 만들었으므로 일부 객체가 해제됩니다. 오버플로우할 블록의 크기도 0x1b0이므로 힙 스프레이의 구멍에 위치할 확률이 높습니다.
memcpy의 대상에서는 블록이 0x1b0 바이트마다 위치합니다:

vftable에 대한 포인터가 LoadLibrary에 대한 포인터로 덮어써집니다:
덮어쓰기 전:

덮어쓰기 후:

결국 **[R11+50]**로 점프하여 끝났으며, 이것이 LoadLibraryA에 대한 포인터임을 기억하세요.
PoC를 실행할 때 DLL을 PoC에 표시된 동일한 경로에 복사합니다:
PoC를 실행하면 DWM 사용자 시스템 무결성 수준 권한으로 CMD 프로세스가 실행됩니다:

참고 자료:
Fortra GitHub의 PoC: https://github.com/fortra/CVE-2024-30051
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-30051
https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2024-30051
이것으로 PoC가 완료됩니다. PoC를 여러 번 실행하면 힙이 불안정한 상태로 남아 있으므로, 다시 작동하게 하려면 머신을 재시작해야 할 수도 있습니다. 또한 첫 번째 시도에서 항상 작동하지는 않을 수 있지만, 일반적으로 두 번째나 세 번째 시도에서는 올바르게 작동합니다. 보시다시피 리버싱은 어려울 수 있으므로, 질문이 있으면 저에게 문의하실 수 있습니다.
X: @ricnar456