深入分析调试CVE-2024-30090-ks.sys内核流服务权限提升漏洞
字数 6210 2025-10-01 14:05:52
CVE-2024-30090 内核流服务权限提升漏洞深入分析与调试教学
0x00 漏洞信息
- 漏洞编号:CVE-2024-30090
- 漏洞类型:内核竞争条件漏洞 (Race Condition)
- 漏洞组件:Windows 内核流媒体服务驱动程序
ks.sys - 漏洞影响:权限提升 (Privilege Escalation)
- 根本原因:Windows 权限验证机制在流媒体服务 (
Kernel Streaming) 处理设备属性请求时存在缺陷,攻击者可通过构造恶意数据结构,利用竞争条件实现任意 IOCTL 调用原语 (Arbitrary IOCTL Primitive),从而以内核权限执行受限操作。
0x01 基本概念
1. ks.sys 的功能
ks.sys 是 Windows 内核流媒体 (Kernel Streaming, KS) 子系统的核心驱动程序。
- 核心作用:提供内核级别的高效多媒体流数据处理。
- 主要功能:
- 实时媒体流处理:支持音频、视频流的实时传输。
- 低延迟:直接在内核操作,为游戏、视频会议等应用提供低延迟保障。
- 资源管理:管理多媒体设备(如声卡、摄像头)的资源分配与通信。
2. ks.sys 的问题
若 ks.sys 出现问题或存在漏洞,可能导致:
- 音频/视频播放异常
- 系统蓝屏 (BSOD)
- 多媒体设备无法正常工作
- 权限提升等安全风险
3. 条件竞争 (Race Condition)
- 官方概念:当多个线程并发访问共享资源(代码、变量、文件等),且缺乏适当的锁或同步机制时发生。
- 在本漏洞中的应用:攻击者通过精确控制两个线程的执行时序,在一个线程(内核)读取并验证数据后、使用数据前,由另一个线程(用户态)篡改该数据,从而绕过安全检查。
4. CPU 亲和性 (CPU Affinity)
- 作用:将特定线程绑定到指定的 CPU 核心上运行。
- 目的:提高竞争条件漏洞利用的成功率。通过将触发线程和干扰线程绑定到不同的物理核心,确保它们能真正并行执行,最大化竞争窗口的利用机会,避免线程调度带来的不确定性。
5. ks 事件 (Kernel Streaming Events)
- 事件集 (Event Set):一组可供侦听器请求通知的相关事件(如设备状态改变、流位置更改)。
- 注册机制:侦听器通过调用
KsSynchronousDeviceControl函数,并使用IOCTL_KS_ENABLE_EVENT控制码,附带指向KSEVENT和KSEVENTDATA结构体的指针来注册事件通知。 - 微型驱动支持:通过提供包含处理例程指针的
KSEVENT_ITEM结构来描述对事件的支持。
0x02 工具与环境
- 调试工具:
- IDA Pro x64:用于静态分析驱动模块。
- WinDbg x64:用于内核动态调试。
- 目标机环境:
- 操作系统:Windows 11 23H2
- 版本号:Build 10.0.22621.3672
- 目标模块:
ks.sys:漏洞所在的核心驱动。ksthunk.sys:与内核流处理相关的另一个模块。
- 调试配置:配置好双机内核调试环境(宿主机调试目标机)。
0x03 利用代码分析
利用代码分为两个部分:Parent 进程 (64位) 和 Child 进程 (实现核心利用)。
Parent 部分
1. GetKernelBase 函数
- 目的:动态获取 Windows 内核模块
ntoskrnl.exe的基地址。 - 原理与步骤:
- 查询所需缓冲区大小:首次调用
NtQuerySystemInformation函数,查询SystemModuleInformation类别,传入 NULL 指针,函数返回所需缓冲区大小dwSize。 - 分配内存并获取信息:根据
dwSize分配内存,再次调用NtQuerySystemInformation获取完整的系统模块信息列表 (PSYSTEM_MODULE_INFORMATION)。 - 遍历查找:遍历模块列表,通过比对模块名(如
\SystemRoot\system32\ntoskrnl.exe)找到ntoskrnl.exe模块。 - 返回基址:返回找到模块的
ImageBaseAddress。
- 查询所需缓冲区大小:首次调用
- 作用:为计算内核符号(如
nt!SeDebugPrivilege)的实际地址提供基准。
2. 传递地址
- 将获取到的内核基址通过命令行参数传递给 Child 进程。
- Parent 进程随后等待 Child 进程执行。
Child 部分
整体流程
- 接收 Parent 传来的内核基址。
- 检测 CPU 核心数:必须 >=2,否则无法实现可靠并行竞争。
- 进入核心利用函数
ExploitIoctlKsEnableEvent。 - 利用成功后,创建 SYSTEM 权限的 cmd 进程 (
spawn_cmd_system)。
1. ExploitIoctlKsEnableEvent 函数
- 目的:操纵内核时钟驱动 (
KSCATEGORY_CLOCK) 的事件处理机制,利用竞争条件篡改内核工作项对象 (KsWorkerObject) 指针,实现任意地址写入或代码执行。 - 关键步骤:
- 初始化数据结构:
- 指定事件类型为
KSEVENT_CLOCK_INTERVAL_MARK。 - 设置
KsWorkerObject指向target_addr - 0x5C(目的是覆盖KSWORKITEM结构中的回调函数指针)。
- 指定事件类型为
- 绑定 CPU 与启动干扰线程:
- 将主线程绑定到特定 CPU 核心。
- 启动
flip_thread线程,高频翻转g_ksevent.Flags(在KSEVENT_TYPE_ENABLE和KSEVENT_TYPE_QUERYBUFFER之间切换),制造竞争条件。
- 主循环(3次尝试):
- 打开设备并启动:打开时钟设备,设置其状态为
KSSTATE_RUN,激活内部事件处理。 - 发送恶意 IOCTL:通过
DeviceIoControl发送IOCTL_KS_ENABLE_EVENT请求。- 输入缓冲区溢出:故意使用超长输入缓冲区 (
sizeof(KSEVENT) + 0x100)。 - 竞争窗口:
flip_thread线程在驱动校验数据后、使用数据前,将Flags从高权限 (QUERYBUFFER) 改为低权限 (ENABLE) 以绕过二次检查。
- 输入缓冲区溢出:故意使用超长输入缓冲区 (
- 成功处理:若
DeviceIoControl成功返回,说明竞争成功,内核会错误地将用户态g_eventData作为工作项处理。等待内核调度执行目标代码。
- 打开设备并启动:打开时钟设备,设置其状态为
- 清理与重试:每次尝试后重置设备状态。3次尝试机制提高稳定性。
- 初始化数据结构:
2. pin_name_to_cpu 函数
- 功能:将当前线程绑定到指定的 CPU 核心。
- 步骤:
GetCurrentThread获取当前线程句柄。- 通过位运算生成目标 CPU 的亲和性掩码(如 CPU0: 0x01, CPU1: 0x02)。
- 调用
SetThreadAffinityMask设置线程亲和性。
3. xCreateThread 函数
- 功能:封装
CreateThread,简化线程创建并增强错误处理。 - 参数:线程函数地址
func,参数lpParam,线程 ID 指针pThreadId,失败退出标志bExitOnFailure。 - 返回值:成功返回线程句柄,失败根据标志退出或报错。
4. 触发循环
- 外层循环:控制成功次数 (
inc_count < 3)。 - 内层循环:持续尝试触发竞争条件。
KsOpenDefaultDevice打开时钟设备。SetClockState(hClockDevice, KSSTATE_RUN)启动设备。- 设置
g_ksevent.Flags = KSEVENT_TYPE_QUERYBUFFER。 - 调用
DeviceIoControl发送恶意请求。 - 若成功,等待 (
Sleep(1500)),停止设备,计数加一,跳出内层循环。
5. spawn_cmd_system 函数
- 目的:创建 SYSTEM 权限的
cmd.exe进程。 - 步骤:
- 获取 Winlogon PID:调用
GetProcessIDByName(L"winlogon.exe")。Winlogon 进程通常以 SYSTEM 权限运行。 - 打开进程句柄:
OpenProcess(PROCESS_CREATE_PROCESS, ...)获取 Winlogon 进程句柄。 - 创建进程:调用
CreateProcessFromHandle(自定义函数),以 Winlogon 为父进程创建cmd.exe,子进程继承 SYSTEM 权限。 - 清理资源:关闭句柄。
- 获取 Winlogon PID:调用
6. GetProcessIDByName 函数
- 功能:通过进程名查找 PID。
- 步骤:
CreateToolhelp32Snapshot创建进程快照。- 使用
Process32First和Process32Next遍历快照。 - 比对
szExeFile字段寻找目标进程。 - 找到返回 PID,否则返回 0。
7. CreateProcessFromHandle 函数
- 功能:创建指定父进程的新进程。
- 步骤:
- 初始化
STARTUPINFOEX结构。 - 初始化进程属性列表 (
InitializeProcThreadAttributeList)。 - 设置
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性,将父进程指定为传入的hProcess。 - 调用
CreateProcess创建cmd.exe,并指定EXTENDED_STARTUPINFO_PRESENT标志使属性生效。
- 初始化
0x04 调试分析与原理解析
1. 调用链与漏洞原理
- 漏洞触发点:
KsIncrementCountedWorker函数中的InterlockedIncrement调用。该函数是一个增量原语 (Increment Primitive),会增加我们提供的地址处的值。 - 核心利用链:
CKSThunkDevice::DispatchIoctl->CKSAutomationThunk::ThunkEnableEventIrp->KsSynchronousIoControlDevice->KspEnableEvent-> ... ->KsGenerateEvent->KsIncrementCountedWorker - 原理简述:
- 设置
Flags = KSEVENT_TYPE_QUERYBUFFER,使请求进入高权限处理路径,此时RequestorMode被设置为KernelMode,绕过许多检查。 - 利用竞争条件,在
CKSAutomationThunk::ThunkEnableEventIrp复制数据到内核缓冲区后、调用KsSynchronousIoControlDevice前,通过干扰线程将Flags改为KSEVENT_TYPE_ENABLE。 - 内核后续处理 (
KspEnableEvent) 看到低权限Flags,省略了严格的检查。 - 最终,恶意构造的
KSEVENTDATA结构被传递给KsIncrementCountedWorker,其参数 (Worker) 被控制为NtSeDebugPrivilegeVA - 0x5C。 KsIncrementCountedWorker内部计算Worker + 0x17*4 (0x5C)正好指向NtSeDebugPrivilegeVA,并对其执行增量操作。
- 设置
2. 为何是 SeDebugPrivilege 和 3 次?
SeDebugPrivilege是内核中的一个全局变量,其值控制着是否允许调试高权限进程。- 普通进程的
SeChangeNotifyPrivilege通常被初始化为0x17(23)。 SeDebugPrivilege被初始化为0x14(20)。nt!PsOpenProcess会调用nt!SeSinglePrivilegeCheck检查当前进程的权限是否与SeDebugPrivilege的值匹配。- 利用目标:将
SeDebugPrivilege的值从0x14增加到0x17,使其与普通进程的权限值匹配,从而绕过检查,获得SeDebugPrivilege权限。 - 3次循环:因为需要增加 3 次 (
0x14->0x15->0x16->0x17)。
3. 关键调试断点与观察
- 断点于
CKSAutomationThunk::ThunkEnableEventIrp:观察传入的 IRP 和请求参数。验证Flags的初始值 (0x400/KSEVENT_TYPE_QUERYBUFFER)。 - 断点于
KsSynchronousIoControlDevice:观察调用参数,确认输入缓冲区内容和RequestorMode已成为KernelMode。 - 断点于
KspEnableEvent:观察分支判断,验证竞争成功后传入的Flags已变为KSEVENT_TYPE_ENABLE,从而走入低权限检查分支。 - 断点于
KsGenerateEvent和KsIncrementCountedWorker:观察NotificationType和传入的Worker参数,确认最终计算出的地址正是nt!SeDebugPrivilege的地址。 - 监视
nt!SeDebugPrivilege内存地址:在调试器中持续查看该地址的值,观察其从0x14经过三次调用后变为0x17的过程。
0x05 复现结果
成功利用该漏洞后:
- Child 进程中的三次竞争条件尝试均成功触发。
nt!SeDebugPrivilege的值从0x14被递增至0x17。- 随后调用
spawn_cmd_system函数。 - 系统成功弹出一个以 SYSTEM 权限运行的
cmd.exe命令提示符窗口。 - 至此,实现了从普通用户权限到最高系统权限的提升。
免责声明:本文档仅用于安全研究与教学目的。请勿将其用于任何非法或恶意活动。