综述
漏洞的核心问题出在 dwmcore.dll 这个 dwm 进程的核心 dll 中,具体问题在函数 CCommandBuffer::Initialize 中,可以看到这里 mempy 操作中的长度由攻击者控制 len_control,目标地址的内存空间分配时 size=(len_control/0x90)*0x90,套用以上公式,如当 len_control=0x95,则实际分配的内存只有 0x90,之后 memory 时将导致 0x5 长度的越界写入。
DWM
而该漏洞主要涉及DirectComposition对象,要触发使用DirectComposition对象需要使用到以下三个核心函数:
-
NtDCompositionCreateChannel
-
NtDCompositionProcessChannelBatchBuffer
-
NtDCompositionCommitChannel
创建 DirectComposition 对象,需要通过 NtDCompositionCreateChannel 系统调用创建一个通道。
typedef NTSTATUS(*pNtDCompositionCreateChannel)(
OUT PHANDLE hChannel,
IN OUT PSIZE_T pSectionSize,
OUT PVOID* pMappedAddress
);
通道创建完毕后,通过 NtDCompositionProcessChannelBatchBuffer 系统调用发送具体的调用命令,每个命令都有各自的格式,命令保存在 NtDCompositionCreateChannel 返回的 pMappedAddress 中。
typedef NTSTATUS(*pNtDCompositionProcessChannelBatchBuffer)(
IN PHANDLE hChannel,
IN DWORD dwArgStart,
OUT PDWORD pOutArg1,
OUT PDWORD pOutArg2
);
enum DCOMPOSITION_COMMAND_ID
RemoveVisualChild
};
typedef NTSTATUS(*pNtDCompositionCommitChannel)(
IN HANDLE hChannel,
OUT PDWORD out1,
OUT PDWORD out2,
IN DWORD flag,
IN HANDLE Object
);
漏洞样本分析
动态获取以下函数的 api 地址。
之后通过函数 fun_hookInit/fun_hookSpecialfunInstall 完成对以下四个函数的 hook:
-
NtDCompositionCommitChannel -
NtDCompositionCreateChannel -
RtlCreateHeap -
RtlAllocateHeap
这里的 hook,总结来说就是 fun_hookInit 函数完成 hook 是中间跳转代码的生成,如下所示:
fun_hookSpecialfunInstall 完成目标函数 hook。
详细来看这四个函数 hook 后进行了哪些操作,其中 RtlCreateHeap/RtlAllocateHeap 是对堆创建和分配的操作,当 RtlCreateHeap 的参数 a2 传入堆对象分配时的基址时,hookpro 保存返回的堆对像为 pointer_specialHeapobject,针对 RtlAllocateHeap 函数,当 RtlAllocateHeap 分配堆时,如果对应的堆对象为前面 RtlCreateHeap hook 时保存的 pointer_specialHeapobject 时,返回此时分配的堆地址,总结一下,这里会监控传入堆基址的堆对象的创建过程,并保存该分配的堆地址。
针对函数 fun_hookProNtDCompositionCreateChannel 的 hook,其会监控第一次 NtDCompositionCreateChannel 的创建,并获取该 Channel 对应的 pMappedAddress,保存为 pointer_pMappedAddress。
针对函数 NtDCompositionCommitChannel 的 hook 就比较复杂了,通过前文我们知道 NtDCompositionCommitChannel 用于生成批处理命令 bufer 并发送到 DWM 进程,这里同样hook的时候会确保是第一次 NtDCompositionCommitChannel 的调用,并在 pointer_specialHeapalloc 这段前面堆分配中获取的内存中搜索 0x120 这个字段,找到将该字段修改为 var_contrulLen,不同环境下的利用有所区别,这里是 0x23f(var_contrulLen(0x1B0)+0x8f),之后 0x23f/0x90 = 4,看到这里我们基本可以猜测,这个位置的 0x23f 就是漏洞中攻击者控制的 len_control。
根据 0x23f/0x90 = 4 进行 4 次拷贝,拷贝的内容为 len_control+0x2c 处开始长度为 0x90 的内存,向后依次保存,此外拷贝的内容中会有两处地址的值被设置为 0。
之后调用四次 NtDCompositionProcessChannelBatchBuffer,这里使用的命令是 SetResourceIntegerProperty,资源号从 1-4,资源对应的 Propertyid 4 被设置为值 1000。
hook 完成后,通过 fun_trigger 函数触发漏洞完成提权操作。
fun_trigger 中首先注册了一个窗口类,调用函数 fun_windowsInit 完成相关窗口的后续定义工作,然后调用函数 DirectComposition::CDevice::Commit。
首先来看 fun_windowsInit,根据前面注册的窗口类,创建了一个名为 test 的窗口。
之后的代码比较复杂,总结起来就是通过 D2D1CreateFactory 创建了一个 2d 绘图的工厂接口,使用工厂接口创建了一个 surface,一个 visual,这里 surface 可以简单理解为一张画布,visual 则可以理解为一个画框,在画布上完成绘制后,放入该 visual 画框,并和前面的 test 窗口关联起来,并在 fun_windowsInit 返回后通过函数 DirectComposition::CDevice::Commit 将当前的 DirectComposition 场景提交给图形硬件进行渲染。
完成 test 窗口图形硬件绘制后,释放出后续的 s1.dll。
根据前面探测的版本进入指定版本的利用流程,这里我们进入 var_osVersiontype=2 的利用类型,函数 NtDCompositionCreateChannel 开启一个 Channel,通过该 Channel 调用 NtDCompositionProcessChannelBatchBuffer,该函数调用 0x10000 次,每次调用传入的指令是 CreateResource,对应创建的资源 id从0x14 到 0x10014,资源类型为 CHolographicInteropTextureMarshaler。
之后同样通过 NtDCompositionProcessChannelBatchBuffer 调用 ReleaseResource 命令释放前面创建的资源,被释放目标资源的 id 从 0x14-0x7000,以 0x20 作为间隔,可以看到这里是一个明显的 spray操作,该步骤结束之后,这片连续的 CHolographicInteropTextureMarshaler 内存中将每隔 0x20 个 CHolographicInteropTextureMarshaler 对象出现一个 hole。
获取前面 hook 中分配的内存指针 pointer_specialHeapalloc 并初始化其指定偏移 v35 之后的内存,将 v35 作为参数传递给函数 fun_gadgetInit,之后调用函数 ShowWindow 将前面通过 DirectComposition::CDevice::Commit 绘制好的图像通过配置好的窗口test显示。
fun_gadgetInit 函数的内容很简单,用于配置前面传入的 v35 中的内容,其中重要的位置有三处:
-
0x0:_fnGESTURE -
0x50:LoadLibraryA -
0x58:s1.dll path
最终 v35 的内存如下所示
最后通过函数 NtDCompositionProcessChannelBatchBuffer 再次调用 ReleaseResource,将剩余的资源释放。
通过静态分析可知漏洞本身是一处越界写入,攻击者利用 spray 的方式,疑似通过越界写入来修改 CHolographicInteropTextureMarshaler 资源,但是具体到利用的细节却又不少疑问:
-
几处 hook 发生的位置及作用是什么? -
hook 中的 pointer_specialHeapalloc 扮演了一个什么样的角色,该内存之后 v35 的内存布局意义何在? -
攻击者控制的长度是如何修改并被最终使用导致越界的? -
漏洞是否是通过破坏 CHolographicInteropTextureMarshaler 来实现的代码执行? -
具体触发漏洞写入的位置在哪里,触发代码执行的位置又在何处?
调试分析
hook 之后可以看到对应的部分被修改为一个 jmp 跳转,最终指向 hookpro 函数。
而这里返回地址中则保存了前面 hook 时被 jmp 覆盖的两条指令,最终指回之前的 syscall 代码部分,从而完成 hook 返回。
分别对这四个 hookpro 函数下断点,来看看具体利用样本中是在哪些关键的地方触发了 hook,首先是针对 NtDCompositionCreateChannel 函数的 hook,该 hook 指会在第一次的调用时触发,通过堆栈回溯可以看到触发第一次 NtDCompositionCreateChannel hook 的位置在 fun_windowsInit 的 DCompositionCreateDevice 位置处。
而触发另外三个函数 hook 的皆在 DirectComposition::CDevice::Commit 将当前的 DirectComposition 场景提交给图形硬件进行渲染的过程中,具体的函数调用流程如下所示。
DirectComposition::CDevice::Commit 中会调用 DirectComposition::CDevice::AllocateSharedMemory 生成一段 ShareMemory,该函数如下位置将触发对应的 RtlCreateHeap/RtlAllocateHeap 的 hook。
DirectComposition::CSharedSection::Create 如下所示,可以看到这里 hook 触发时 RtlCreateHeap 的基址实际上一段通过 MapViewOfFile 映射的内存,其本质上一个 CShareSection 资源,DirectComposition::CSharedSection 是 Windows DirectComposition API 中一个重要的类,它用于在不同的 DirectComposition 视觉对象之间共享内存 CSharedSection,本质上是一个内存块,它被映射到不同的进程空间中,也可用于 dwm 进程和 win32kbase.sys 中用于数据共享。
之后调用 DirectComposition::CSharedSection::Allocate,在这段 CSharedSection 上分配一段长度为 0x228 的内存,该分配的内存地址会被 RtlAllocateHeap hook 捕获并保存为 pointer_specialHeapalloc,DirectComposition::CDevice::AllocateSharedMemory 返回后 pointer_specialHeapalloc+0x74 的位置作为参数传入 DirectComposition::CPrimitiveGroup::WriteCommandBuffer 中用于设置 CommandBuffer,细心的朋友可以发现,漏洞触发核心函数为 CCommandBuffer::Initialize,正是 CCommandBuffer 对象的初始化过程,利用代码中攻击者通过 2d 的绘制图形并关联对应的窗口后渲染 DirectComposition::CDevice::Commit,当 DirectComposition::CDevice::Commit 将当前的 DirectComposition 场景提交给图形硬件进行渲染的过程中 hook 修改对应 CSharedSection 中的 CCommandBuffer 触发漏洞。
DirectComposition::CDevice::Commit 中相关函数调用流程如下所示。
可以看到当 DirectComposition::CDevice::Commit 触发 NtDCompositionCommitChannel hook 时,此时 pointer_specialHeapalloc 这段 CSharedSection 内存如下:
-
0x48 处为对应的长度字段,也就是触发漏洞的核心,攻击者通过 hook 可以控制 -
0x74 的位置开始是 CCommandBuffer,其由一个个 0x90 长度的内存块构成。
NtDCompositionCommitChannel hook 完成之后,对应的 control_len 已经被修改为 0x23f,后续的 CCommandBuffer 内存块被复制为 4 个,因为最终漏洞函数中 control_len/0x90 = 0x23f/0x90 = 4,内存块的数量需要满足。
CSharedSection 中恶意 CCommandBuffer 完成构造后,进入漏洞的 spray 部分,这里通过分配 0x10000 个 CHolographicInteropTextureMarshaler 资源实现 dwm 进程中堆对象的创建,可以看到一个 CHolographicInteropTextureMarshaler 对象的大小是 0x1B0。之后每隔 0x20 个资源编号释放一个 CHolographicInteropTextureMarshaler 对象,这将导致连续的 CHolographicInteropTextureMarshaler 对象堆空间中出现一个个 0x1B0 大小的内存 hole。
这里可以看到生成的 CHolographicInteropTextureMarshaler 对象的堆大小为 0x1B0。
CHolographicInteropTextureMarshaler 对象创建时的堆栈如下:
构造 pointer_specialHeapalloc 中 CommandBuffer 第四个 0x90 内存块,即前面提到的 v35。
通过 fun_gadgetInit 函数构造完成该内存块结构如下所示:
当 Showwindow 函数运行,会将 DirectComposition::CDevice::Commit 函数提交给图形硬件进行渲染的效果最终在 test 窗口中展示出来,并触发漏洞函数 CCommandBuffer::Initialize。
漏洞函数 CCommandBuffer::Initialize 中攻击者控制的恶意 buffer 有第二个参数,类型为 ID2D1PrivateCompositorBuffer。
通过堆栈回溯可知该参数在函数 CD2DSharedBuffer::CreateFromSharedSection 中生成,该函数的第一个参数就是利用程序和 dwm 的共享内存,第二个参数则是对应修改的长度 23f。
CD2DSharedBuffer::CreateFromSharedSection 中 ID2D1PrivateCompositorBuffer 生成很简单,该对象长度为 0x28,以共享内存地址 /size 的格式写入。
生成的 ID2D1PrivateCompositorBuffer 如下所示:
正式进入漏洞函数 CCommandBuffer::Initialize,此时的堆栈调用如下,当 showwindow 将 DirectComposition::CDevice::Commit 函数提交给图形硬件进行渲染的效果最终在 test 窗口中展示出来时,将触发漏洞函数。
CCommandBuffer::Initialize 中进入漏洞逻辑 exp len_control = 23f,(2f3/90)*0x90 =1b0,最终计算处分配的内存为 0x1b0,可以完美填充分配到前面 spray 的 CHolographicInteropTextureMarshaler 对象 hole 中,而实际需要的长度为 0x23f,这将导致 8f 长度内存的溢出。
分配 0x1b0 大小的内存。
由于前面 spray 的操作该分配的 0x1b0 大小的空间将直接分配到 CHolographicInteropTextureMarshaler 序列内存中的 hole 中,形成以下的内存形式:
CHolographicInteropTextureMarshaleri + hole + CHolographicInteropTextureMarshaleri1
此时一旦完成拷贝,CHolographicInteropTextureMarshaleri1 对象将被破坏,如下所示第一个红框就是我们分配的 0x1b0 的目标拷贝地址,而随后第二个红框中的内容就是 CHolographicInteropTextureMarshaleri+1 对象。
此时拷贝的内容是我们前面通过 hook pointer_specialHeapalloc 构造的恶意 CommandBuffer,其中核心是红框中的两个指针。
这里 00007ffa819ad5f0 就是一处虚指针。
覆盖后的效果如下,这两处指针第一处为 _fnGESTURE,第二处为 LoadLibraryA。
针对第一处的 _fnGESTURE 指针下内存读断点。
可以看到当样本尝试释放剩余所有 CHolographicInteropTextureMarshaler 资源时。
内存读断点断下,此时正是针对 CHolographicInteropTextureMarshaler 资源的释放操作。
这里 dwmcore!CComposition::Channel_DeleteResource 函数中获取对应被破坏 CHolographicInteropTextureMarshaler 对象处的恶意指针并针对该 poi(evilpointer)+0x10 进行调用,攻击者设置的恶意指针将最终通过以上方式寻址找到函数 __fnTOUCHHITTESTING。
__fnTOUCHHITTESTING 函数如下所示,会直接将 rcx+0x50 作为函数地址,rcx+0x28 作为函数第一个参数。
进入到 __fnTOUCHHITTESTING,此时的 rcx 内容如下,可以看到 rcx+0x50 指向了恶意构造的 loadLibrary,rcx+0x28 此时的值为 0x58。
但是当 FixupCallbackPointers 函数执行完毕后,此时 rcx+0x28 就指向了 0x58 处的 dll path。
最终导致任意 dll 加载,实现代码执行。
S1.dll 已经被加载。
现在来整体总结下该漏洞利用的过程:
1. 通过 D2D1CreateFactory 配合 DirectComposition::CDevice::Commit 在窗口 test 上进行绘制,该调用会导致 DirectComposition::CDevice::Commit 的过程中生成对应的 CommandBuffer,该 CommandBuffer 被利用中的 hook 修改。
2. 调用 NtDCompositionProcessChannelBatchBuffer 在 dwm 进程中 spray 一系列 CHolographicInteropTextureMarshaler 资源,并每隔 0x20 个释放掉一个该资源,生成一系列 hole。
3. 通过 showwindow 显示 test 窗口,这将导致 DirectComposition::CDevice::Commit 提交的 CommandBuffer 在 dwm 进程的 CCommandBuffer::Initialize 漏洞函数中被调用,从而占据一个 CHolographicInteropTextureMarshaler 释放的 hole 并实现对紧随其后的 CHolographicInteropTextureMarshaler1 资源的复写。
4. 调用 NtDCompositionProcessChannelBatchBuffer 释放剩余的 CHolographicInteropTextureMarshaler 资源,这将导致被复写 CHolographicInteropTextureMarshaler1 资源中恶意修改的函数指针被调用,从而通过恶意构造的 __fnTOUCHHITTESTING 以第一个参数可控的方式调用 loadlibrary 加载恶意 dll。
详细分解如下图所示:
利用加载
shellcode 会简单解密出一个 s2.exe。
s2.exe 首先同样会通过 RtlDecompressBuffer 释放出一个 s3.dll,之后执行 fun_triggerWinloginload 函数。
fun_triggerWinloginload 中首先通过系统版本初始化两个变量。
之后动态获取一些系统 api 的地址,并通过和之前利用程序中一致的方式 hook 函数 MapViewOfFile。
MapViewOfFile 的 hookpro 函数中首先会执行 MapViewOfFile,并 sleep 一段时间后判断对应返回的映射内存偏移 0x10 的位置是否是 0x1FFEEFFEE。
之后开始对这段 MapViewOfFile 进行赋值操作。
赋值的内容如下所示:
由之前的分析可知,dwm 作为 window 中桌面管理显示的进程,其他进程和 dwm 的通信,例如我们的 exp,都是通过 dcomp 和内核 win32kbase.sys 进行,并在 win32kbase.sys 中生成对应资源的并反馈给 dwm 进程,这就有一个问题,如果对应进程和 dwm 之间通信需要很大的数据,这时再通过内核进行中转将是一件很麻烦的事情,因此诞生了前面提到的资源 CSharedSection,以用于进程和 dwm 之间进行数据的共享,这里的共享其实就是我们前面漏洞利用中提到的 DirectComposition::CDevice::AllocateSharedMemory。
只是前面的利用中是修改了该段 CSharedSection 0x74 之后的 CCommandBuffer,而通过观察可知 CSharedSection 中还保存了通信进程中一些对象的虚表指针,此时 s2.exe 位于 dwm 的进程,因此可以通过 hook 指定的 MayViewOfFile 获取该段 CSharedSection 并修改其中的虚表指向类似利用中的 _fnGESTURE 来加载任意 dll,从而控制对应的通信进程行为,如果该通信的进程是 system 权限,则可以起到提权的效果,该样本中则是尝试修改的目标进程就是 consent.exe,这里漏洞利用样本最后执行 ShellExecuteA(0i64, “runas”, “C:\Windows\System32\cmd.exe”, 0i64, 0i64, 1);来实现 consent.exe 和 dwm 的交互。
实际上当 dwm 进程加载了 s1.dll 的时候,就已经完成了提权,攻击者绕这么大的弯通过 hook dwm 进程的 MapViewOfFile 来修改 CSharedSection 中 consent.exe 进程对象的虚表指针,以加载 dll 实现提权更多应该是为了绕过相关安全软件的保护。可以看到最终 consent.exe 这个 uac 进程加载了 s3.dll,并通过 s3.dll 弹出了一个 root shell。
此处对应的调用堆栈如下所示:
主利用 exp 中调用 ShellExecuteA(0i64, “runas”, “C:\Windows\System32\cmd.exe”, 0i64, 0i64, 1),这将导致触发一个管理员的 cmd,从而触发 uac,其调用流程是 svchost->consent.exe,此时 consent.exe 将和 dwm 进行一次渲染操作,consent 进程中将调用了函数 dcomp!DirectComposition::CSharedSection::Allocate,用于分配一段 CSharedSection 的共享内存。
这里返回后可以看到函数返回后对应的内存中被放置了两个指针。
两处虚表分别指向以下两个函数:
-
CMILRefCountImpl::AddReference
-
DirectComposition::CCompositorSynchronizedObject::SafeToModify
而同时在我们的 dwm 中,s3 会触发对应的共享内存的映射,从而被设置的 hookpro 捕获,这段内存对应映射在 consent 中的地址为 0x1f16b6c120,而在前面的分析中知道 consent 中分配的地址从 0x1f16b6c720 开始。
因此 hookpro 中的操作就是,构造了一段恶意的指针调用内存,并向后写入 0x1f16b6c720 处的两个指针,将其指向这段共享内存低地址处构造的恶意指针调用内存,如下所示,当这段内存映射回 consent 进程时 0x1f16b6c720 中的指针就已经指向了恶意构造的恶意的指针调用内存。
此时 consent.exe 中的共享内存已经被修改( 720 及 740 的位置)。
uac 触发完成时调用 dcomp!DirectComposition::CDelayedDestructionObject::Release,该函数中会使用到前面修改的第一个位置处的指针 CMILRefCountImpl::AddReference。
如下所示,最终 dcomp!DirectComposition::CDelayedDestructionObject::Release 中的调用方式 poi(poi(evilpoint)+0x18) 指向了我们恶意内存中构造的 combase!NdrProxyForwardingFunction3 指针。
combase!NdrProxyForwardingFunction3 函数如下所示,其代码很简单。
总结一下,该代码会以以下伪代码执行 poi(poi(evilpoint+0x20)+0x18)(poi(evilpoint+0x20)),其最终结果就是通过第二个原 DirectComposition::CCompositorSynchronizedObject::SafeToModify 处被替换的指针寻址到恶意内存中构造的 USER32!_fnCOPYDATA 函数,并将该替换地址作为参数。
如下此时进行 USER32!_fnCOPYDATA 的调用,参数就是原第二处替换指针的地址。
USER32!_fnCOPYDATA 和前面的 USER32!__fnTOUCHHITTESTING 类似,即当可以控制第一个参数的情况下,用于实现 loadlibaray 函数任意 dll 加载的小工具函数。
USER32!_fnCOPYDATA 中参数为恶意构造内存中的恶意虚表2处,以该虚表地址依此获取偏移 0x68 处和 0x28 处恶意内存构造好的 loadlibrary 和 s3 路径,从而调用 loadlibrary 加载最终的 s3 代码,该代码实现很简单直接弹出一个 cmd。
自此我们总结以下整个利用的后利用部分:
1. exp 中通过触发漏洞获取在 dwm 中的代码执行权限,并在成功时 dwm 加载第一阶段的 s1.dll。
2. s1.dll 释放出一段 shellcode 并执行,该 shellcode 释放 s2.exe 并执行。
3. exp 中执行 ShellExecuteA(0i64, “runas”, “C:\Windows\System32\cmd.exe”, 0i64, 0i64, 1);来实现 consent.exe 和 dwm 的交互,consent.exe 进程中生成对应的 sharedSection。
4. s2.exe 在 dwm 中执行,会 hook dwm 中 mapviewofile,当监控到指定的内存映射时越界修改 consent.exe 进程中生成对应的 sharedSection 中的两处虚表。
5. consent.exe 中 sharedSection 中的两处虚表触发执行,进入攻击者设置的恶意虚表指针,通过 NdrProxyForwardingFunction3/__fnCOPYDATA/LoadLibraryA 执行流加载最终 s3.dll 并创建一个 cmd,实现最终的提权。
最终我们将漏洞利用的流程图和后续提权利用的流程合并,整个样本的利用技术流程就可以总结成以下的一张大图。
QakBot,也称为 QBot、QuackBot 和 Pinkslipbot,是一种已经存在十多年的银行木马。它于 2007 年在野外被发现,此后一直得到不断的维护和开发,过去十几年中并没有发现 QakBot 相关攻击活动中有使用 0day 的案例,直到今年上半年出现的 CVE-2024-30051。通过上文的分析,可以看到整个样本的开发者对 Windows 内部的机制研究得非常透彻,分析过程中,笔者也常为其利用中使用得手法而拍案叫绝,作为一款历史悠久的银行木马,QakBot 有着足够的经济实力进行 0day 军备的补充,因此目前我们无法断定该漏洞利用样本是否是 QakBot 内部自研,但是正如 2023 年末奇安信威胁情报中心发布的年度报告中就提到,种种迹象都表明越来越多非传统意义 APT 团伙,且有经济实力的攻击组织,如勒索,再到如今的老牌银行木马都疑似通过网络军火商购入并频繁使用 0day 漏洞,且这样的行为在未来将越发普遍。
参考链接
[2] https://www.zerodayinitiative.com/blog/2021/5/3/cve-2021-26900-privilege-escalation-via-a-use-after-free-vulnerability-in-win32k
[3] https://github.com/progmboy/cansecwest2017
[4] https://blog.talosintelligence.com/snapshot-fuzzing-direct-composition-with-wtf/
[5] https://securelist.com/cve-2024-30051/112618/
[6] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30051
点击阅读原文至ALPHA 7.0
即刻助力威胁研判
原文始发于微信公众号(奇安信威胁情报中心):公开的隐秘:CVE-2024-30051在野提权漏洞研究