0x00 前言
最近,windows的Kernel Streaming框架出现了许多漏洞,本文通过分析CVE-2024-38054来了解一下Kernel Streaming这个攻击面。
CVE-2024-38054由 Angelboy提交给微软,在今年7月份修补的一个堆溢出漏洞。该漏洞在ksthunk.sys。驱动的作用是将WOW64(Windows 32-bit on Windows 64-bit)程序对使用Kernel Streaming框架的设备发起irp请求转化为64位的irp请求。以便后续处理无需区分wow64程序和正常64位程序。
目前由Fr0st1706编写的CVE-2024-38054的poc已经在GitHub公开。
0x01 Kernel Streaming框架简介
Kernel Streaming框架主要为windows中的音频、视频等设备提供支持。Kernel Streaming框架提供三种多媒体类驱动模型: port类, stream类和 AVStream类。
- port 类是指用于 PCI 和基于 DMA 的音频设备。
- stream类是指视频类设备。
- AVStream 是 Microsoft 提供的多媒体类驱动程序,支持纯视频流和集成音频/视频流。
kernel steam架构如下所示:
由于音视频类的驱动大多是pnp类型的驱动,在windows中的设备不是类似\Devcie\NamedPipe的形式。而是类似下面形式:
\\?\root#system#0000#{cf1dda2c-9743-11d0-a3ee-00a0c9223196}\{cfd669f1-9bc2-11d0-8299-0000f822fe8a}&{cf1dda2c-9743-11d0-a3ee-00a0c9223196}
如果想枚举pnp设备的路径的话,比较方便的方式是使用windows自带的工具:
pnputil /enum-interfaces
也可以使用WDK提供的工具KsStudio来查看使用Kernel Streaming框架的驱动,我目前在win10下面测试正常,win11 ksmon.sys加载不上去。以wdk 10.0.26100.0为例,该程序的位置在:C:\Program Files (x86)\Windows Kits\10\Tools\10.0.26100.0\x64
中。该工具显示信息比较详细,对于某一项还是会有缺失。
另外一个缺点是筛选的种类有限,例如mskssrv.sys的类别是KSSTRING_Server 类别是{3C0D501A-140B-11D1-B40F-00A0C9223196}
就没有在列表中。
在Kernel Streaming框架中有两个比较重要的内核对象种类:KS Filters 和 KS Pins
ks filters是由微型端口驱动提供的filter factory创建的,用户态程序可以发起irp请求基于filter实例创建ks pin,然后使用ks pin实现向filter提交nodes 或者读取经过filter的数据。
0x02 pnp设备注册逻辑
以mskssrv.sys为例,可以在C:\Windows\INF\ks.inf
中DeviceRegistration节中找到注册逻辑
可以看到在DeviceRegistration节中对HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
写入
rundll32.exe streamci,StreamingDeviceSetup {96E080C7-143C-11D1-B40F-00A0C9223196},{3C0D501A-140B-11D1-B40F-00A0C9223196},{3C0D501A-140B-11D1-B40F-00A0C9223196}
这个表示只会执行一次,执行streamci.dll的StreamingDeviceSetup
函数,在reactos dll\win32\streamci\streamci.c
有相关函数的实现
在StreamingDeviceSetup函数中首先分割参数,然后调用InstallSoftwareDeviceInterface函数注册设备
#define STATIC_BUSID_SoftwareDeviceEnumerator \
0x4747B320L, 0x62CE, 0x11CF, {0xA5, 0xD6, 0x28, 0xDB, 0x04, 0xC1, 0x00, 0x00}
在InstallSoftwareDeviceInterface函数中,首先搜索STATIC_BUSID_SoftwareDeviceEnumerator的实例路径,然后进行DeviceIoControl code IOCTL_SWENUM_INSTALL_INTERFACE,参数包含DeviceID即{96E080C7-143C-11D1-B40F-00A0C9223196}、 InterfaceId即 {3C0D501A-140B-11D1-B40F-00A0C9223196} 与pnputil查询的结果一致,即可以通过inf的DeviceRegistration节来判断某个pnp实例路径是属于哪一个驱动的。
0x03 补丁对比
驱动改动三个函数:
- CKSThunkPin::GetExtendedHeaderSize
- CKSThunkPin::LocateFormatHandler
- CKSThunkPin::ThunkStreamingIrp
CKSThunkPin::GetExtendedHeaderSize中的改动如下:
首先判断flag是否含有KSSTREAM_HEADER_OPTIONSF_METADATA标识,如果含有则返回的size+0x10。KSSTREAM_HEADER_OPTIONSF_METADATA标识用于KSSTREAM_HEADER结构体的OptionsFlags成员,其含义是在KS_FRAME_INFO结构体后有一个KSSTREAM_METADATA_INFO 结构体。
KSSTREAM_HEADER_OPTIONSF_METADATA标识一般会配合KSSTREAM_HEADER_OPTIONSF_FRAMEINFO标识一起使用,该标识的含义是KSSTREAM_HEADER结构体后会有一个KS_FRAME_INFO 结构体。
KSSTREAM_HEADER结构体如下所示:
typedef struct {
ULONG Size;
ULONG TypeSpecificFlags;
KSTIME PresentationTime;
LONGLONG Duration;
ULONG FrameExtent;
ULONG DataUsed;
PVOID Data;
ULONG OptionsFlags;
ULONG Reserved;
} KSSTREAM_HEADER, *PKSSTREAM_HEADER;
需要注意的是改结构体的size成员,由于KSSTREAM_HEADER可以扩展,所以该成员的值不能小于sizeof(KSSTREAM_HEADER)。例如当同时具有这两个标识时内存布局如下所示:
- KSSTREAM_HEADER结构体
- KS_FRAME_INFO 结构体
- KSSTREAM_METADATA_INFO 结构体
此时KSSTREAM_HEADER结构体的Size成员的值应不小于sizeof(KSSTREAM_HEADER)+sizeof(KS_FRAME_INFO)+sizeof(KSSTREAM_METADATA_INFO )
通过查看CKSThunkPin::GetExtendedHeaderSize函数交叉引用
可以看到CKSThunkPin::GetExtendedHeaderSize函数只在CKSThunkPin::ThunkStreamingIrp中调用
CKSThunkPin::ThunkStreamingIrp中的改动是:
如果KSSTREAM_HEADER结构体OptionsFlags字段,如果包含KSSTREAM_HEADER_OPTIONSF_METADATA标识,KSSTREAM_HEADER结构体的size成员应不小于0x88即sizeof(KSSTREAM_HEADER)+sizeof(KS_FRAME_INFO)+sizeof(KSSTREAM_METADATA_INFO )的大小。
CKSThunkPin::LocateFormatHandler的改动如果interface为null也可以返回。
0x04 漏洞逻辑触发
通过补丁对比可以看出漏洞大概率发生在CKSThunkPin::ThunkStreamingIrp函数中,通过交叉引用可以在看出CKSThunkPin::ThunkStreamingIrp函数只在CKSThunkPin::DispatchIoctl函数中调用,调用的条件是改irp请求是32位程序发起的且RequestorMode为1即用户态且IoControlCode是0x2F4017或者0x2F8013。
然后再往上交叉引用就索引不到了。
在看ksthunk的driverEntry函数,是哪个函数处理deviceIocontrol的irp请求的。
可以看出DriverEntry主要是调用了CKernelFilterDriver::Initialize()函数进行初始化。
DriverObject的type的偏移是0,加上v3(0x70)即是majorfunction 的偏移,这个do while循环实际上是将所有的major function设置为CKernelFilterDevice::DispatchIrpBridge函数
CKernelFilterDevice::DispatchIrpBridge函数是CKernelFilterDevice::DispatchIrp函数的包装。
这里DeviceExtension成员应该是CKernelFilterDevice的子类,对于不同的majorfunction请求调用不同的子类虚函数进行处理。
通过调试poc可以得到完整的堆栈。
可以看出在CKernelFilterDevice::DispatchIrp函数中调用CKernelFilterDevice的子类CKSThunkDevice的CKSThunkDevice::DispatchIoctl成员函数处理,然后在CKSThunkDevice的CKSThunkDevice::DispatchIoctl函数中CKSThunkPin::DispatchIoctl函数处理。
0x05 CKSThunkPin::ThunkStreamingIrp函数分析
__int64 __fastcall CKSThunkPin::ThunkStreamingIrp(__int64 a1, PIRP a2, __int64 a3, int *a4)
{
....
v55 = a2->Tail.Overlay.CurrentStackLocation;
OutputBufferLength = v55->Parameters.DeviceIoControl.OutputBufferLength;
if ( (unsigned int)OutputBufferLength < 0x30 ) // OutputBufferLength不能小于0x30即sizeof(KSSTREAM_HEADER结构体)大小
{
.... // 返回错误码
}
....
PoolWithTag = (LONGLONG *)ExAllocatePoolWithTag((POOL_TYPE)512, OutputBufferLength, 0x6874734Bu);
....
v9 = a2->UserBuffer;
if ( v55->Parameters.DeviceIoControl.IoControlCode == 0x2F8013 ) // 根据IoControlCode 使用ProbeForRead或者ProbeForWrite校验UserBuffer地址
....
memmove(PoolWithTag, a2->UserBuffer, OutputBufferLength); // 根据OutputBufferLength拷贝到内核态的内存中 防止double fetch
a2->AssociatedIrp.MasterIrp = (struct _IRP *)PoolWithTag;
...
v12 = (KSSTREAM_HEADER_32 *)PoolWithTag; // UserBuffer是一个KSSTREAM_HEADER_32数组
...
if ( *a4 >= 0 )
{
while ( (_DWORD)OutputBufferLength && (unsigned int)OutputBufferLength >= v12->Size )
{
v53 = v11 + 1;
v16 = v12->Data;
if ( v16 )
{
ProbeForWrite((volatile void *)v16, v12->FrameExtent, 1u); // 校验KSSTREAM_HEADER_32的FrameExtent成员的地址是否合法
if ( v12->DataUsed > v12->FrameExtent )
ExRaiseStatus(0xC0000206);
v17 = v12->OptionsFlags;
if ( !v15 )
{
v17 &= 0xFFFE7FFF;
v12->OptionsFlags = v17;
}
if ( (v17 & 0x18000) == 0
&& !IoAllocateMdl((PVOID)(unsigned int)v12->Data, v12->FrameExtent, Irp->MdlAddress != 0i64, 1u, Irp) )
{
ExRaiseStatus(-1073741670);
}
MetadataBuffer32 = GetMetadataBuffer32((struct _KSSTREAM_HEADER32 *)v12);
... //校验MetadataBuffer32的地址
}
ExtendedHeaderSize = 0;
v21 = v12->Size;
if ( (unsigned int)v21 > 0x30 ) 如果有扩展头,则获得扩展头的size,扩展头的size为KSSTREAM_HEADER_32成员size的值-0x30
{
...
ExtendedHeaderSize = CKSThunkPin::GetExtendedHeaderSize(
(CKSThunkPin *)a1,
v12->OptionsFlags,
(unsigned int)(v21 - 0x30));
LODWORD(v21) = v12->Size;
LODWORD(v13) = v52;
}
LODWORD(v13) = ExtendedHeaderSize + 0x38 + v13; // 64位buffer的size为ExtendedHeaderSize+0x38 即KSSTREAM_HEADER_32成员size+8
v52 = v13;
LODWORD(OutputBufferLength) = OutputBufferLength - v21;
v12 = (KSSTREAM_HEADER_32 *)((char *)v12 + (unsigned int)v21);
v14 = a1;
v15 = v60;
v11 = v53;
}
......
}
....
v26 = (char *)ExAllocatePoolWithTag((POOL_TYPE)512, v13 + 0x10, 0x6874734Bu); // 根据while循环算出的size分配v13 +0x10的64位buffer
...
v32 = (struct KSSTREAM_HEADER *)(v26 + 0x10); // 新分配的buffer+0x10 作为起始
...
v34 = v53;
if ( !v53 )
goto LABEL_77;
while ( 1 ) //对64位buffer进行赋值
{
.....
v36 = *((_DWORD *)PoolWithTag + 11);
v32->OptionsFlags = v36;
v37 = v36 & 0x8000;
....
if ( !v37 )
break;
....
LABEL_64:
if ( GetMetadataBuffer32((struct _KSSTREAM_HEADER32 *)PoolWithTag) ) // 如果metaBuffer32即拥有KSSTREAM_HEADER_OPTIONSF_METADATA的标识
{
if ( MdlAddress && (MdlAddress = MdlAddress->Next) != 0i64 || (v32->OptionsFlags & 0x8000) != 0 )
{
v40 = GetMetadataBuffer64(v32); // 获得metaBuffer64的地址即v32+0x80的位置
v42 = v40;
if ( v40 )
{
v40->BufferSize = *v41;
v40->Flags = v41[4];
v40->Reserved = v41[5]; // 即v32+1c的位置 当KSSTREAM_HEADER_32成员size+8<0xa0时发生溢出
v40->UsedSize = v41[1];
v40->SystemVa = 0i64;
....
}
}
}
...
}
....
}
可以看出这两个while循环主要是做一个32位结构体到64位结构体的转换:
- KSSTREAM_HEADER在32位大小为0x30,在64位为0x38。
- KSSTREAM_METADATA_INFO 结构体在32位大小是0x18,而64位为0x20。
- KSSTREAM_METADATA_INFO结构体相对于KSSTREAM_HEADER 在32位的偏移是0x70 在64位的偏移为0x80。
当内存结构为下面的情况的时候:
- KSSTREAM_HEADER
- KS_FRAME_INFO
- KSSTREAM_METADATA_INFO
在32位的总体大小为0x88,在64位的总体大小为0xa0
而在算64位的size的算法是32位size-0x30+0x38 以size为0x88为例,最终的size为0x90,而实际要写0xa0 导致10字节的溢出。
0x06 PoC分析
溢出原理如图所示:
该PoC获得mstee.sys实现ks Filters实例路径后,调用KsCreatePin函数获得ks pin的实例
之后调用DeviceIoControl触发漏洞。0000000000000000000000000000000000000000000000000000000000000000000
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
该PoC使用了两个KSSTREAM_HEADER第一个header作为padding,使得64位结构体分配的buffer为0x4000,通过漏洞溢出1字节修改namepipe的DATA_QUEUE_ENTRY的NextEntry的flink成员的低位使其指向nextEntry的data字段 从而控制链表指向用户态,从而控制整个数据结构实现任意地址读写,DATA_QUEUE_ENTRY结构体如下:
struct DATA_QUEUE_ENTRY {
LIST_ENTRY NextEntry;
_IRP* Irp;
_SECURITY_CLIENT_CONTEXT* SecurityContext;
uint32_t EntryType;
uint32_t QuotaInEntry;
uint32_t DataSize;
uint32_t x;
char Data[];
}
exp的布局如下图所示:
0x07 总结
文章首先介绍了漏洞所在驱动的作用,然后通过漏洞成因以及补丁分析确定了漏洞函数。重点分析了该漏洞的原理,阐述了漏洞exp的原理并给出了exp布局图。总体来说,该漏洞的漏洞点是比较隐蔽的,不像传统的堆溢出有明显的内存拷贝操作。希望通过本文的介绍可以让读者在日常的漏洞挖掘中获得新的思路。
0x08 参考链接
- Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I
- Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II
- https://ssd-disclosure.com/ssd-advisory-ksthunk-sys-integer-overflow-pe/
- 枚举已安装的设备
- windows-learning CVE-2024-38054
- Windows-Non-Paged-Pool-Overflow-Exploitation