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 FiltersKS 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 参考链接

  1. Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I
  2. Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II
  3. https://ssd-disclosure.com/ssd-advisory-ksthunk-sys-integer-overflow-pe/
  4. 枚举已安装的设备
  5. windows-learning CVE-2024-38054
  6. Windows-Non-Paged-Pool-Overflow-Exploitation