Windows DWM 核心库特权提升漏洞 (CVE-2024-30051)(2024 年 8 月 15 日发布)

Windows DWM 核心库特权提升漏洞 (CVE-2024-30051)(2024 年 8 月 15 日发布)

Ots安全 2024-09-08 14:37

在这篇博文中,我将解释我在开发 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 进行二进制差异比较,展示堆溢出是如何产生的,然后通过提升权限来利用它,最后创建一个可以正常运行的 PoC。

漏洞详细信息:

Windows DWM 核心库特权提升漏洞 CVE-2024-30051

发布日期:2024 年 5 月 14 日

分配 CNA:Microsoft CVE-2024-30051

影响:权限提升

最大严重性:重要

弱点:

CWE-122:基于堆的缓冲区溢出

CVSS:3.1 7.8 / 7.2

该漏洞是由于主 Windows DWM 库dwmcore.dll中的整数除法中存在大小计算错误而导致的 。本地用户可以在dwmcore.dll中的CCommandBuffer::Initialize方法中导致堆上的缓冲区溢出,并可以使用具有 Integrity 系统权限的DWM用户执行任意代码。该漏洞将在DWM进程中执行堆喷射以准备内存,并最终在dwmcore.dll中产生堆溢出, 这将通过释放堆喷射的某些部分来触发。

一旦漏洞利用成功,DWM 进程将加载我们精心设计的 DLL,以具有 Integrity 系统权限的 DWM 用户身份执行我们的代码或可执行文件(在我们的例子中为 CMD)。

让我们来看看这个漏洞,看看它如何允许我们以完整性级别为 SYSTEM 的 DWM 用户身份运行。请注意,由于这不是属于管理员组的用户,因此它有一些权限限制

通过 diff 查找 Bug:

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中分配的大小,第二次调用获取memcpy的相同大小。

一切最初看起来都正确。然而,在分配之前,它会对大小执行一些操作。

它通过调用相同的 CD2DSharedBuffer::GetBufferSize函数来获取buffer_size和buffer_size2,并返回相同的值。但在new中,它执行预操作,将 buffer_size除以0x90,然后乘以 0x90,而在memcpy中,它使用返回的buffer_size2而不对其进行操作。

通过这些操作我发现new 和memcpy最终使用的大小可能不一样。

buffer_size = buffer_size2(返回的大小)

size_new=缓冲区大小/0x90 x 0x90

size_memcpy=buffer_size2

例如,如果buffer_size是0x91

缓冲区大小 = 缓冲区大小2 = 0x91

size_new =缓冲区大小/0x90 x 0x90 = 0x90

size_memcpy = buffer_size2 = 0x91

此示例证明存在堆溢出。它复制的字节数超出分配的字节数,并且大小是可控的。

例如,如果 buffer_size 是0x23f,就像在 POC 中使用的一样。

缓冲区大小 = 缓冲区大小2 = 0x23F

size_new =缓冲区大小/0x90 x 0x90 =0x1b0

size_memcpy == buffer_size2=0x23f

分析完漏洞函数后,我想看看如何到达漏洞函数CCommandBuffer::Initialize。事情开始变得复杂了。

回顾对此函数的引用,它似乎是通过CPrimitiveGroup类的方法到达的:

可以从CPrimitiveGroup对象的vftable访问此类方法 :

它有其构造函数:

可以通过以下方式实现:

当我最初经历这个过程时,我花时间阅读了 PDF “ DirectComposition 的失落世界:寻找 Windows 桌面窗口管理器错误”,并深入了解了 Direct Composition 的世界。这帮助我创建了我的第一个 PoC。

另外,我需要逆向win32ksys并尝试通过以下函数发送包:
– 函数创建通道

  • NtDCompositionProcessChannelBatchBuffer

  • 提交通道

我的第一个 PoC 到达了CPrimitiveGroup构造函数。然而,经过多次逆向,我还是没有找到一种方法来处理对 vftable方法的调用,从而直接通过ALPC调用使用这些函数来到达易受攻击的函数 。

我花了很多时间进行一些复杂的逆向分析。在此过程中,我发现了利用该漏洞的恶意软件样本,这非常有帮助,因为利用方法比我最初想象的要复杂得多。它还包括几个与系统 API 的挂钩,并使用了一些可能有点可疑的方法。但在战争和漏洞利用中,任何东西都是有效的,所以我开始分析恶意软件,并从该分析中创建了最终利用该漏洞的最终 PoC,我将在下面进行解释。

首先,我想澄清的是,该恶意软件不仅利用了 CVE-2024-30051漏洞将我们的进程提升到 Integrity 系统级别,而且还执行了第二部分,最终提升了具有所有权限的 SYSTEM 用户权限,这已经超出了 CVE 的解释。

此外,值得注意的是,该恶意软件比我试图最小化代码的 PoC 复杂得多。该恶意软件执行了更多检查以确保可靠性,因此它在第一次尝试时就成功了。我放弃了所有这些检查以简化并专注于纯粹的利用,甚至可能需要运行 PoC 两三次才能实现利用。

利用CVE-2024-30051的PoC分析:

1)初始化

可执行 PoC 的链接为 https://github.com/fortra/CVE-2024-30051

首先,PoC 调用GetVersion来获取正在运行的操作系统版本,并据此对一些全局变量执行不同的初始化。我的 PoC 在 Windows 11 23H2 和 Windows 11 22h2 上进行了测试。其他系统也存在漏洞,并添加了要利用的值。

2)挂钩

它钩住了四个系统函数,如果不钩住它们,就无法实现利用。这些系统函数是: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 在内存中的映射彼此完全分开,它们之间有空白空间。

让我们看看钩子是如何工作的:

它调用一个钩子函数,该函数将执行RtlAllocateHeap API的钩子操作,该函数有三个参数,第一个是要修补的 API 的地址,称为 sym_RtlAllocateHeap。

在修补之前,它指向 API 的开头:

以下是RtlAllocateHeap函数:

第二个参数是名为hook的例程,它将在 API 完全修补后执行:

钩子函数调用my_RtlAllocateHeap。

挂钩函数将修补 api 的前 5 个字节,以便它跳转到hook。

它将调用分配区域中的代码,在该区域中执行第一个使用 5 个字节步进的 API 指令,然后 在修补的字节之后跳转到RtlAllocateHeap+5 :

钩住后,API 看起来是这样的。前 5 个字节发生了变化,因此它跳转到hook。它将调用my_RtlAllocateHeap(就在上方的代码),然后返回到标记为紫色的区域以继续执行 API:

当 API 执行完毕后,它将返回到钩子。从那里,它将比较全局变量heap_base(初始为零 )与传递给RtlAllocateHeap的第一个参数:

之后,代码等待某个特殊的分配,该分配具有特定的HeapHandle。一开始这个变量为零,只要它为零,它就会跳过并像普通的 RtlAllocateheap 一样工作:

参数HeapHandle是在RtlCreateHeap内部获取的,巧合的是,它是第二个被挂钩的 API。

寻找对全局变量heap_base的引用,它只在hook2函数中更改其值,该函数是在 hook RtlCreateHeap 之后执行的:

因此,我们的想法是捕获某个HeapHandle并将其保存在 heap_base中。由于它现在不为零,因此函数 钩子将开始比较每个分配。因此,PoC 将保存具有与之前存储的HeapHandle相同的地址内存。

在这种情况下,它会将分配的方向保存到名为base 的变量中:

前两个钩子现在已链接在一起。当hook2保存HeapHandle的预期值时,它会激活将保存使用相同HeapHandle的分配地址的钩子函数。

第三个钩子被提交给NtDCompositionCreateChannel。第一次调用时,它将保存 MappedAddress ,即第三个参数的内容。从那里开始,它会将hooked_flag更改 为 1,因此从那时起它将不再保存并正常工作。

变量base中保存的地址稍后将被读取三次。其中两次将出现在最后一个钩子中,称为hook4:

到NtDCompositionCommitChannel的函数hook4将在后面进行分析,因为它比较复杂,而且非常重要。

3)创建窗口

四个钩子完成后,它返回到主函数开始创建窗口。这是通过调用RegisterClassExW完成的。但是,要注册一个窗口类以供以后使用,应该使用CreateWindowExW函数进行调用。

通过调用CoInitializeEx来初始化 COM 库,以供调用线程使用:

它根据所需尺寸计算窗口矩形所需的尺寸:

调用函数CreateWindowExW来创建将要绘制的窗口:

4)创建设备

从那里,调用D3D11CreateDevice来创建代表显示适配器的设备或 DirectX 设备:

在我的 PoC 中,ppDevice被命名为d3dDevice,ppInmediateContext 被命名为d3dContext:

参数标志需要设置为0x20:

然后调用AddRef:

这将增加指向 COM 对象的接口指针的引用计数器:

将值 0x10 减去以下结果:

在ID3D11Device-0x10的偏移量0xf8处有一个指向 TComObject的指针:

这将是新的THIS并最终跳转到 TComObject::AddRef:

最后它把位于 TComObject偏移量 8 处的对象计数器加一:

然后,AddRef会增加在D3D11CreateDevice中创建的另一个对象类型的计数器,该对象类型为ID3D11DeviceContext:

在这种情况下,为了找到新的THIS,它会减去 0x108:

它跳转到这里,在偏移量 0x98 处有新的THIS:

这是计数器。在本例中,它是一个QWORD:


5)创建工厂

PoC 调用D2D1CreateFactory来使用 Direct2D,并创建 ID2D1Factory接口,该接口用于创建可用于绘制或描述形状的其他 Direct2D 资源:

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

由于我不是直接创作方面的专家,因此我使用了与恶意软件相同的步骤:

返回的新工厂没有提供任何详细类型。它显示为 void *,这意味着它没有正式记录:

由于我不知道这种情况下的对象类型,因此我开发了一个可执行文件,使用它可以轻松地在内存中看到它:

在四个钩子函数中添加断点。在本例中,当hook2捕获到HeapHandle 时,会显示一个断点:

当捕获到所需的块时,应该停止钩子:

在另外两个钩子上放置断点:

然后它继续调用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

它尝试执行一种动态转换。如果 ID3D11Device类型的对象可以接受IDXGIDevice的接口(使用方法等) ,它会创建原始对象的副本,该副本接受新类型,然后返回指向它的指针。在这种情况下,变量d3dContext1将是IDXGIDevice 类型:

两个对象都继承自CLayeredObject

原始的ID3D11Device是

就像返回指针的那个。

然后使用CreateDevice函数 创建一个ID2D1Device对象:

在value2中它返回一个 ID2D1Device 类型的对象。

6)创建设备上下文

此时,PoC 从Direct2d 设备创建一个新的设备上下文。使用函数CreateDeviceContext

https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext

这是它在 PoC 中的实现:

7)创建组合装置

然后调用DCompositionCreateDevice

https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatedevice

IID 属于 _ IDCompositionDevice

8)调用DCompositionCreateDevice函数

在跟踪DCompositionCreateDevice函数的同时 ,它在hook3处停止,此时它调用 NtDCompositionCreateChannel:

这样,它就可以捕获调用DCompositionCreateDevice时系统内部使用的MappedAddress :

这是到目前为止的调用堆栈:

这是dcomp模块调用函数 NtDCompositionCreateChannel的地方:

9)创建处理 HWND 的目标

从上一步返回后,保存 MappedAddress 。使用 ALPC,它将连接到DWM进程,然后调用 CreateTargetForHwnd

https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createtargetforhwnd

它使用了创建的窗口的句柄HWND。它与我刚刚创建的设备相关,也就是这个方法的THIS :

10)创建表面

然后调用CreateSurface

https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createsurface

11)调用BeginDraw、EndDraw和CreateVisual

然后调用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

12)调用可视化设置内容

接下来,它调用IDCompositionVisual::SetContent:

https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionvisual-setcontent

并调用SetRoot:

BeginDraw中收到的updateObject没有在文档中指定其类型。

13)释放对象

接下来,它释放以前创建的对象:

14)提交组合设备

现在使用相同的IDCompositionDevice类型的 dcompDevice对象,它调用 Commit 方法:

15)调用hook2

调用Commit方法会在捕获所需 HeapHandle 的 hook2上停止:

这是现在的调用堆栈:

请记住,可以使用CPrimitiveGroup类的某些方法访问易受攻击的函数。此时它会创建一个Heap,然后hook2捕获并保存相应的HeapHandle。

16)调用函数钩子

在返回主函数之前,它还使用 RtlAllocateHeap创建一个块。然后将其捕获并存储在钩子函数内的基变量中:

对Create和Allocate 的调用是依次执行的:

两者(Allocate和Create)均从 DirectComposition::Cdevice::Commit 调用:

17)调用函数hook4

此后,当调用 NtDCompositionCommitChannel时,它会在hook4处停止:

NtDCompositionCommitChannel从这里调用:

它也从DirectComposition::Cdevice::Commit 调用

值得一提的是,系统已经将ALPC发送给DWM 的命令进行了批处理。
之后,它使用 NtDCompositionCommitChannel 发送命令。

函数hook4会拦截NtDCompositionCommitChannel 调用,此时将向批处理中添加更多命令。

让我们看看hook4做了什么:

对指向base 的块执行一个循环。

当在块内找到值0x120时,它退出循环:

它存储了0x120值所在的地址和偏移量:

它用value4覆盖0x120值,该值等于 0x1b0 + 0x8f = 0x23f。
这是溢出时在memcpy中将使用的大小 :

它将0
xbc + 0x90添加到0x120所在的地址指针 :

请记住,在偏移量0x48处,基址的大小为0x120。
该大小被0x23f覆盖,因此原始块的大小必须是 0x120:

源是0x23f + 0x2c的指针地址:

它最初添加了0x90,但现在又减去了0x90。

目标将是指向 0x120 + 0xbc 的指针的地址:

它将在此写入:

所有的书写内容都在块内:

它将重复循环3次,这是0x1b0/0x90整个除法的结果:

此后,由于ArgChannelHandle通道与捕获MappedAddress时使用的通道相同。
PoC 将使用NtDCompositionProcessChannelBatchBuffer将命令添加到批处理中。
这些命令将与系统添加的命令一起处理。
批处理收集这些命令,然后使用 NtDCompositionCommitChannel 将命令一起发送:

发送的命令的值为 8,对应 4 个不同跟踪器(1、2、3 和 4)的SetResourceIntegerProperty 。

18)执行堆喷射

当PoC返回到主函数时,它会创建一个不同的通道来执行HeapSpray。

它批处理 0x10000 个命令,这些命令通过 _ NtDCompositionCommitChannel 发送:

这使用值CreateResource=1和与CHolographicInteropTextureMarshaler = 0x50相对应的类型:

分配在下面的代码中执行。
为进行喷射而创建的对象的大小为0x1b0:

然后它执行一个循环来释放上一步创建的对象,并在内存分配中产生漏洞。

变量counter2从0x3000开始,当它小于0x7000时,添加 0x20 的步长:

19)发送前修改基本块

它从基数+ 0x48 + 44 + 0x1b0的块方向写入 0x41s,

也就是说,它写入的是稍后将在溢出相邻块时使用的值:

pvalue7位于基址0x224地址上:

然后进入函数“ escribe ”:

它写入pKernelCallbacktable加上0x388、 LoadLibraryA 地址和将要加载的 DLL 的路径。
在本例中,我将其命名为s11.dll。

20)调试 DWM 进程

现在,需要内核调试器在发生堆溢出时停止在易受攻击的函数处。这是因为无法使用用户模式调试器调试 DWM 进程。

使用IDA PRO远程调试目标,设置条件断点,当大小等于0x1b0时停止:

print ("VALUE1 %x" % ((cpu.rax)))
return cpu.rax==0x1b0.


于正在从内核调试用户模式程序,因此需要切换到DWM进程上下文来放置断点。
使用以下命令重新加载用户符号:

. reload /user

使用以下命令重新加载内核:

. reload /f

当ShowWindow被跨过时它将停止:

它分配大小为0x1b0 的空间并复制大小为0x23f 的空间, 从而产生堆溢出:

此时调用堆栈如下所示:

为了创建溢出,DWM 接收以下代码中的值:

我的 PoC 发送的基础中精心设计的值是使用模块 dwmcore.dll中的DWM 进程中的MapViewofFile读取的 :

前一个函数的调用来自:

当使用ALPC从hook4使用destination_copy (NtDCompositionCommitChannel)发送时它会停止:

请记住,在hook4命令中,命令被添加到批处理中。但是,系统也已经将一些命令添加到批处理中,包括基础命令和精心制作的数据:

在这种情况下,它共享一个从000001cd’178d0000开始的内存区域 。当它被用作执行memcpy的源时, 它将位于同一内存区域后面的0x794字节处。

共享内存区域的大小为0x4000:

当需要分配的大小为0x1b0时就会停止,到达 memcpy复制0x23f字节:

内存中0x1b0以外是将溢出并覆盖相邻块的代码:

当 PoC 释放块时,它会跳转到 LoadLibraryA,加载精心设计的库:

出自这里:

堆喷射是由CHolographicInteropTexture类型的 0x1b0大小的对象进行的。

由于我在内存分布中打了洞,这会释放一些对象。由于即将溢出的块大小也是0x1b0 ,因此它很有可能位于堆喷射的洞中。

在memcpy的目标位置,块每隔 0x1b0 个字节分布一次:

指向vftable的指针被指向 LoadLibrary 的指针覆盖 :

覆盖之前:

覆盖后:

请记住,它最终跳转到[R11 + 50],这是指向LoadLibraryA的指针。

21)将权限提升至诚信系统级别

执行 PoC,将 DLL 复制到 PoC 中所示的相同路径中:

执行 PoC 后,将使用DWM用户 Integrity 系统级别权限执行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。请记住,如果多次执行该操作,堆将保持不稳定状态,因此您可能需要重新启动机器才能使其再次工作。此外,虽然它可能并不总是在第一次尝试中起作用,但它通常会在第二次或第三次尝试中正常工作。如您所见,逆向可能很困难,因此如果您有任何疑问,可以咨询我。

邮箱:[email protected]

X:@ricnar456

https://github.com/fortra/CVE-2024-30051?tab=readme-ov-file

感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里