CVE-2024-35250是DEVCORE发现的一个可利用性极高的内核提权漏洞(发现者是著名大佬Angelboy)
也可以阅读Angelboy大佬的文章Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I | DEVCORE
内核流式传输
在进行漏洞分析之前需要先了解一些前置知识
Windows内核流式传输框架(Windows Kernel Streaming Framework)是Windows操作系统中用于处理实时多媒体数据流的核心组件
当打开摄像头、开启音效以及麦克风等音频设备时,系统需要将这些设备中的声音、影像等相关数据读取到 RAM 中, 这时就会用上内核流式传输框架
内核流有三种多媒体驱动模型:
- 端口类
- AVStream
- 流类(已经过时, 现代设备基本不会采用该模型)
端口类
主要用于音频硬件驱动(如声卡等)
由微软提供端口驱动(Port Driver),处理通用音频功能(如混音、格式转换)。硬件厂商只需实现微型端口驱动(Miniport Driver),专注于硬件控制。
AVStream
适用于视频采集、流媒体处理(如摄像头、视频捕获卡)
取代了旧版 KS(Kernel Streaming) 框架,提供更现代的驱动模型。
支持即插即用(PnP)、电源管理,简化了过滤器(Filter)和管脚(Pin)的实现。
设备交互
对于这类设备其实交互与普通设备并没有太大区别, 同样是通过CreateFile打开设备获得句柄进行操作
设备路径
但区别在于他们的名字并不像\Devcie\NamedPipe
这样,而是会像下面这样的路径 :
1 | \\\hdaudio#subfunc_01&ven_8086&dev_2812&nid_0001&subsys_00000000&rev_1000#6&2f1f346a&0&0002&0000001d#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\ehdmiouttopo |
组成成分
前缀
\\?\
表示使用 Windows 的扩展长度路径格式(支持超长路径名)
设备类型标识
hdaudio
:表示设备为 高清音频控制器(如 Intel HD Audio)类似还有:
usb#vid_xxxx&pid_xxxx
(USB 摄像头)pci#ven_xxxx&dev_xxxx
(PCI 视频采集卡)
硬件标识符
ven_8086
:厂商 IDdev_2812
:设备型号nid_0001
:节点 IDGUID 部分
{6994ad04-93ef-11d0-a3cc-00a0c9223196}
:Windows 定义的设备接口类 GUID(此处是 KSCATEGORY_AUDIO,表示音频设备)其他常见 GUID:
- 摄像头:
{65E8773D-8F56-11D0-A3B9-00A0C9223196}
- 视频采集:
{53172480-4791-11D0-A5D6-28DB04C10000}
- 摄像头:
功能端点
ehdmiouttopo
:表示设备的特定功能端点(此处是 HDMI 音频输出拓扑)
枚举设备
由于硬件配置、厂商 ID、设备实例 ID 等差异,音频/视频设备的路径(如 \\?\hdaudio#...
)是动态生成的,不能硬编码在代码中
要使用 Windows 提供的设备管理 API(如 SetupDi*
系列函数)动态获取设备路径
SetupDi*
系列核心api
SetupDiGetClassDevs
获取指定设备类别(如音频、摄像头)的所有设备列表。参数:
ClassGuid
:设备类别的 GUID(如KSCATEGORY_AUDIO
)。Flags
:控制枚举范围(如DIGCF_PRESENT
只枚举当前连接的设备)。
SetupDiEnumDeviceInterfaces
遍历设备列表,获取每个设备的接口信息(包括设备路径)。SetupDiGetDeviceInterfaceDetail
获取设备的详细路径(即\\?\hdaudio#...
格式的字符串)。
当然也可以直接使用ks简化的api, 快速打开指定类别的第一个可用设备
1 |
|
本质还是对 SetupDiGetClassDevs
+ CreateFile
的封装
内核流对象
Windows内核流式传输框架(Kernel Streaming)在启用设备后会在内核中创建关键对象实例
KS过滤器(KS Filter)
采用类似”黑盒”的设计理念,开发者通过统一接口与过滤器交互
每个KS过滤器通常代表一个物理设备或设备的特定功能模(不仅限于物理设备,也可以用于虚拟设备或软件层面的处理), 作为数据处理的中心枢纽,所有流数据都要通过过滤器进行处理
例如: 打开音频设备后会对应到一个音频过滤器,过滤器可能由多个节点组成,节点对流数据进行处理。音频过滤器通常会处理音频数据流,但它可能包含多个子功能(如解码、编码、效果处理等)
KS引脚(KS Pin)
作为过滤器的数据输入/输出端点, 必须通过Pin实例才能对Filter进行数据读写操作
主要作用有:明确区分输入端和输出端, 定义支持的数据格式和传输特性, 控制数据流的方向和行为
核心属性系统
所有KS对象(过滤器和Pin)都通过属性系统暴露其功能, 使用GUID标识的属性标识, 例如支持的格式、传输特性等
开发者可以使用IOCTL_KS_PROPERTY来设置和获取属性,控制设备的行为
例子
例如应用程序从视频摄像头读取数据的流程大致如下图
设备初始化:
使用
CreateFile
或KsOpenDefaultDevice
获取设备句柄Pin实例创建:
在过滤器上创建特定Pin的实例, 获取代表该Pin的独立句柄
流配置:
使用
IOCTL_KS_PROPERTY
进行属性设置, 例如视频格式(如MJPG/YUY2), 帧率(如30fps)等同时将Pin状态设置为运行(Run)状态
数据采集:
使用
IOCTL_KS_READ_STREAM
从Pin读取视频帧数据
内核流式传输架构
整个内核流式传输架构大致如下图
其中包含两个重要的驱动ksthunk.sys与ks.sys
ksthunk
内核流式 WOW Thunk 服务驱动程序
在调用 DeviceIoControl 应用后,在 Kernel Streaming 的入口点 ,但它功能很简单,负责将 WoW64 进程中的 32 位请求转换成 64 位请求,使得下层的驱动就不必为 32 位结构另外处理。
ks
内核连接和流架构库
内核流媒体的核心组件之一,它是内核流媒体的库函数,负责转发 IOCTL_KS_PROPERTY 等请求到对应设备的驱动程序中,同时也会负责处理 AVStream 的相关功能。
IOCTLKS* 的工作流程
当调用 DeviceIoControl 时, 用户的请求处理流程大致就如下图
而到第 6 步时 ks.sys 就会根据请求的 属性 来决定要交给哪个驱动及处理器来处理请求
最终再转发给相应的驱动程序,如上图最后转发给 portcls 中的 handler 来操作音频设备
漏洞分析
作者的原文中还详细地描述了漏洞发现的思路与分析过程, 感兴趣可以看作者的文章
这里我们就直接进行漏洞的介绍了
漏洞成因
_ETHREAD线程对象有一个字段PreviousMode, 用于标识参数值的来源是用户模式还是内核模式
PreviousMode - Windows drivers | Microsoft Learn
其有两种可能UserMode
和KernelMode
, 该值会影响内核是否启用某些操作来保障安全性
调用方式 | PreviousMode 设置 | 说明 |
---|---|---|
用户模式应用调用 NtXxx |
自动设为 UserMode |
系统调用陷阱处理程序(如 syscall / sysenter )会设置 PreviousMode = UserMode 。 |
内核驱动调用NtXxx |
保持调用线程的原有值 | 如果线程原本是用户模式调用链的一部分,PreviousMode 可能仍是 UserMode ,导致错误。 |
内核驱动调用 ZwXxx |
强制设为 KernelMode |
ZwXxx 是 NtXxx 的包装器,会临时覆盖 PreviousMode 为 KernelMode ,避免安全检查。 |
与之相关有IRP的RequestorMode字段, 在内核驱动中常常会用到这个字段决定是否需要对用户的requests做一些额外的安全检查
很多时候RequestorMode的值就来自于PreviousMode
那么这个机制就存在一个问题: 即如果用户态发送IRP请求进入内核态后调用了Zw函数, 而Zw\函数本身又发起了一个IRP请求
此时PreviousMode已经变为KernelMode, 但是后来发起的IRP请求却依然可能包含有用户传递的内容, 从而使用户的请求规避了某些安全检查
事实上这种情况确实存在
内核驱动中可以使用 IoBuildDeviceIoControlRequest 这个方法去创建一个 DeviceIoControl 的 IRP,该函数会创建好 IRP,然后后续去调用 IofCallDriver,这样就可以在内核驱动中调用 IOCTL
If the caller supplies an InputBuffer or OutputBuffer parameter, this parameter must point to a buffer that resides in system memory. The caller is responsible for validating any parameter values that it copies into the input buffer from a user-mode buffer. The input buffer might contain parameter values that are interpreted differently depending on whether the originator of the request is a user-mode application or a kernel-mode driver. In the IRP that IoBuildDeviceIoControlRequest returns, the RequestorMode field is always set to KernelMode. This value indicates that the request, and any information contained in the request, is from a trusted, kernel-mode component.
根据微软的文档可以知道, 如果没有特别去设置 RequestorMode 就会直接以 KernelMode 形式去调用 IOCTL
漏洞所在
在ks.sys中就存在对IoBuildDeviceIoControlRequest
方法的使用
通过反编译ks.sys可以发现这个函数KsSynchronousIoControlDevice
完美符合条件
1 | NTSTATUS __stdcall KsSynchronousIoControlDevice( |
尽管这并不是一个Zw*函数, 但是RequestorMode却作为一个参数可控
并且ks.sys中在使用这个函数时, 往往将RequestorMode设置为0
例如UnserializePropertySet
memmove的第二个参数其实应该是CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer
也就是用户可控的内容
OutBuffer也来自原先的IRP请求, 也就是说到这里已经拥有了RequestorMode为KernelMode且存在用户可控参数的原语了
那如何触发UnserializePropertySet
函数呢
在 Kernel Streaming 的 IOCTL_KS_PROPERTY
功能中,为了提高效率
提供了 KSPROPERTY_TYPE_SERIALIZESET
和 KSPROPERTY_TYPE_UNSERIALIZESET
功能允许用户通过 单次调用 与多个 Property 进行操作。
当我们使用这个功能时,这些请求将被 KsPropertyHandler 函数分解成多个调用
过程如图
UnserializePropertySet处理又会调用KsSynchronousIoControlDevice 重新做一次 IOCTL,而此时新的 Irp->RequestorMode
就变成了 KernelMode(0)
EoP
现在已经存在KernelMode下的任意IOCTL_KS_PROPERTY
, 现在要做的就开始寻找可触发利用点
在ksthunk.sys中DispatchIoctl
负责对请求进行分发
如果请求不是32位的, 就会调用CKSThunkDevice::CheckIrpForStackAdjustmentNative
其中3080195是IOCTL_KS_PROPERTY的调用号
而在CKSThunkDevice::CheckIrpForStackAdjustmentNative
函数存在如下代码
如果是直接从用户态调用这个过程, 那么会检查RequestorMode直接返回错误
但现在是从内核态发起的调用, 那么就会执行下面的将用户输入作为函数调用, 并且第一个参数可控
那现在要面对的就是KASLR, SMEP, KCFG等保护
对于前两者在Windows下解决都不算困难
- KASLR使用
EnumDeviceDrivers
或NtQuerySystemInformation
等api即可获得内核加载基址 - SMEP则直接调用内核代码即可
CFG保护会检查通过指针间接调用的函数是否在合法的函数表中
RtlSetAllBits就是一个能够被利用的函数, 其是 kCFG 中合法的 function,而且也只需要控制一个参数_RTL_BITMAP
1 | struct _RTL_BITMAP |
1 | void stdcall RtlSetAllBits(PRTL_BITMAP BitMapHeader) { |
不过这样就不能用替换token的提权方式了, 只能直接修改Token->Privilege将其全部置位来开启所有权限
POC
网络上目前已经有公开的poc
1 | /* |
提权之前的过程就是设置各个属性以及标志位让执行流成功到达目的代码, 并触发函数调用, 遇之前的漏洞描述符合一致
该poc除了原文提到的提权方案之外, 还提供了一个类似的, 共两种提权方式
- 一种是上面提到的使用RtlSetAllBits修改_SEP_TOKEN_PRIVILEGES直接开启所有权限, 然后打开一个高权限进程并以这个进程句柄为父句柄创建新进程
- 还有一种是通过RtlClearAllBits将_ETHREAD.PreviousMode改为0, 从而有权限调用NtWriteVirtualMemory进行内核空间任意写, 替换Token, 在启动新进程之前还原回UserMode, 避免system触发BSOD
修复方案
修复方案在ks!KspPropertyHandler
函数中
修复之前
解析到对应的KsProperty_flag为KSPROPERTY_TYPE_SERIALIZESET
或者KSPROPERTY_TYPE_UNSERIALIZESET
就直接调用对应函数
修复之后
可以看到在调用之前增加了条件检查
除非满足Feature_3118115132__private_IsEnabledDeviceUsage
函数返回True, 而大多数时候显然并不会
否则就比较pInBufProperty->Set
是否为KSPROPSETID_DrmAudioStream
, 不相等才继续调用函数执行
而pInBufProperty->Set == KSPROPSETID_DrmAudioStream
却是在ksthunk!CheckIrpForStackAdjustmentNative
函数中触发函数调用点的前提