本文旨在阐明有关 Windows 漏洞利用的一般方面。它解释了应用于 CVE-2022-37969 的基本概念。最终结果是一个可用的 PoC。它并未阐明该 CVE 的每个方面,但提供了可重用的代码片段,并解释了许多常见漏洞利用中可找到的机制。
目标用户是初级的逆向工程师、漏洞利用开发者,他们希望获得一个可用的概念验证源代码来测试和理解基本的 Windows 内部机制。本文为进一步学习提供了参考点。
要求:基本的内核调试、基本的逆向工程、基本的 Windows 内部机制、C/C++ 编程技能
程序是在机器上运行的一段代码。通常,程序接收数据(输入),使用输入进行计算,并生成数据(输出)。大多数程序由人类编写,因此存在 bug。bug 是由于某些源代码编写不正确(程序员想对输入做某件事,但生成的代码与预期结果不同)而产生的。大多数 bug 在产品发布前已被纠正,但有些仍然存在。这是因为存在不同类型的 bug,有些比其他更难发现。
Windows 是一个计算机程序,由人类编写,因此它也有 bug。为什么这很重要?因为 Windows 系统可以运行处理敏感数据的程序,如银行账户、医疗数据库等。一些 bug 可以被用来非法获取受限数据的访问权限(这是漏洞利用的一个好用例)。
存在多种 bug,有些有用,有些没用。通常,bug 是由程序的输入与写错的代码行共同作用,导致程序产生异常输出或行为。找出那个输入是安全专家(或黑客)的工作。下一步是评估产生的异常输出/行为,并回答“它能否被以有用的方式利用?”。这就是 bug 被分类成不同类别的地方。例如,一个 bug 可能导致破坏某些数据结构,使目标计算机重启。它的用途有限。一个 bug 可能导致输入被写入控制受限文件访问权限的内存区域。这种 bug 更有用。
因此,黑客从所有可能的 bug 集合中搜索对其目的最有用的子集。通常来说,问题是:“我能否向目标程序提供一个特制的输入,使得我不破坏系统,但能够提升我的访问权限并获利?”
经过这一非技术性介绍,本教程的范围可以表述为:我们能否找到一个 Windows 程序,它接受畸形输入,并由于开发者代码错误,能够将我们的权限从普通用户非法提升到管理员?
目标程序:Windows CLFS(公共日志文件系统驱动程序)
Expoit name:CVE-2022-37969
类型:本地权限提升
易受攻击的 ISO 下载:在此下载
Windows 地址空间大致分为用户空间(运行一般程序)和内核空间(运行操作系统本身和硬件组件软件——驱动程序)。普通用户不得访问内核空间,但存在一些机制,普通用户程序可以通过它们访问部分内核代码(系统调用、驱动程序过程)。为什么需要访问?为了以安全且可控的方式与操作系统交互,这是操作系统设计者提供的。
一些驱动程序使用用户提供的数据输入来操作内核空间数据结构。如果输入产生 bug,则内核可能被破坏。一个例子是 Common Log File System Driver。通过使用一些特殊输入,我们可以强制驱动程序改变保存用户权限访问级别的内核数据结构,并用 管理员 覆盖 普通用户。
为了将权限提升到管理员,需要修改什么?
我们首先明确最终目标。Windows 在一个名为 _EPROCESS 的内核数据结构中存储系统中每个运行进程的信息。_Eprocess 示例
一个重要的字段是 struct _EX_FAST_REF Token。这是另一个数据结构,进一步指向引用相应进程权限级别的数据。在下图中,System 进程有一个系统令牌,Explorer 进程有一个普通用户令牌。

因此,要提升 Explorer.exe 的权限,我们需要将 System 的 _EPROCESS-->Token 的值复制到 Explorer 的 _EPROCESS-->Token。我们将通过将 System Token 复制到我们自己程序的 Token,并从提升后的进程启动命令提示符(子进程继承父进程的令牌)来实现类似的操作。
要完成这些操作,我们需要以下机制:
引言:Windows 多年来的性质:随着新漏洞的发现,Windows 需要通过补丁来缓解这些漏洞。同时,随着新技术的出现,Windows 需要更新以保持竞争力。一个关键需求是与先前版本的向后兼容性。有时安全性通过模糊性来实现。数据结构和函数定义从手册中移除,但功能仍然保留。通过逆向工程,研究人员能够将这些功能用于各种目的。
为了找到 _EPROCESS 的内核地址,我们将使用一个未文档化的函数:NtQuerySystemInformation(参数见链接)。通过使用 SystemInformationClass 参数,我们可以指定要检索的信息类型。我们将通过指定 SystemExtendedHandleInformation 值(#define SystemExtendedHandleInformation 0x40)来检索一般进程信息。
使用 NtQuerySystemInformation 的一个注意事项是,我们事先不知道返回数据的长度,但 NtQuerySystemInformation 有一个帮助机制。如果用一个错误大小的数组来请求所需数据,它会返回 ERROR 并告知应该请求的正确数据大小。这可以用于以下面正确的方式读取进程信息:
SystemInformationLength 参数调用 NtQuerySystemInformationReturnLength 参数值SystemInformationLength 值再次调用 NtQuerySystemInformation返回的数据结构类型是 PSYSTEM_HANDLE_INFORMATION_EX。这是一个未文档化的数据结构。(见链接)它引出一个 SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX 数据结构,其 Object 字段包含相应进程的 _Eprocess 数据结构的内核地址。
因此,逻辑将是遍历所有 PSYSTEM_HANDLE_INFORMATION_EX 元素,将 SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX 的 UniqueProcessId 字段与目标进程 PID 进行比较,并选择相应的 Object 字段以在内核中找到其 _Eprocess 地址。
代码片段:

NtQuerySystemInformation 被声明为一个函数指针,其地址在运行时通过 loadlibrary 和 getprocaddress 动态获取。
typedef NTSTATUS(WINAPI* ptr_NtQuerySystemInformation)(int, PVOID, ULONG, PULONG);ntdll=LoadLibrary(L"ntdll.dll");MyNtQuerySystemInformation = (ptr_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");对于读取和写入 Token,我们将依赖于 clfsw32.sys 和命名管道中的漏洞。
这有点像一个黑盒,在其他文章中有详细说明,但这里将解释此功能所需的最少知识,以便获得对漏洞利用过程的基本理解。管道是进程间通信机制。进程可以使用管道相互传递信息。管道表示为内核数据结构,其中一些字段可以从用户空间填充。一个例子是管道属性。
(稍后我们将把这与 CLSF 漏洞联系起来,以获得从用户空间在内核空间中的任意读写能力)
如需进一步阅读,请查阅 fengshui-spraying-big-kids-pool。机制的简化解释如下:
内核根据我们要分配的大小有两种分配内存的方式:用于小于 4KB+头部的对象的小池(small-pool),和用于大于 4KB+头部的对象的大池(big-pool)。大池页很重要,因为可以从用户空间枚举它们。这意味着普通用户可以找到所有包含大池页的内核起始地址。
如何做到?每个大池页都有一个名为 Tag 的字段(可用于获取有关存储在那里的数据类型的信息)。系统中的所有大池页可以使用 NtQuerySystemInformation 并以 SystemBigPoolInformation 作为 SystemInformationClass 参数值来枚举。然后,我们可以从所有页面中按 Tag 过滤,并获得我们感兴趣的大池页的地址。例如,CLFS 使用带有 'Clfs' 标签的大池页。我们可以获取内核中分配了 CLFS 对象的所有页面的地址。

回到管道,我们可以以相同的方式操作,分配一个足够大以使用大池机制的管道,枚举大池页,搜索管道特定的 Tag 并过滤那些页面。
因此,我们可以在用户空间中泄漏内核分配我们管道的位置。那么,如何控制进入内核的数据并读取它呢?
为此,我们依赖管道属性。与大池标签类似,管道属性是一个数组,可以包含描述管道的信息(由用户填充)。使用未文档化的函数 NtFsControlFile,我们可以任意读写管道属性数据结构。由于未文档化,只提供了一个设置 PipeAttribute 向量然后读取它的概念验证。允许对读写原语进行的唯一修改是控制输入/输出缓冲区的内容及其大小。
这里我们分配一个足够大的管道以使用大池页(0x2000),将输入和输出缓冲区设置为受控值。注意,输入值的前两个字节必须为 0x5a 0x00 才能工作。

这里我们读回之前在内核中写入的内容,同样只更改输出缓冲区。

结果:

以下是内核中管道大池页的内容:通过查询大池页,我们找到了内核中管道数据结构的起始位置。在地址 pipe_begin+0x20 处,我们发现一个指向我们的输入缓冲区+0x2 的指针。

当我们调用管道属性读取函数时,操作系统将执行以下操作:
为什么这有用?想象一下,如果我们能够将 pipe_begin+0x20 处的指针替换为 System 进程安全令牌的位置,然后调用管道属性读取,我们就能在用户空间中泄漏该令牌的值。替换是通过 CLFS.SYS 漏洞完成的。
理解这一技术对于理解漏洞利用至关重要。
大多数漏洞利用在本质上不是确定性的,而是概率性的。即使利用漏洞的代码是正确的,漏洞利用也可能不工作。通过将目标程序置于不稳定状态,漏洞利用开发者必须确保在执行漏洞利用后系统不会崩溃。想象一个程序,当被利用时,它提供了从依赖于程序中变量值的地址进行读取的能力。
例如:
假设 target=0x1000000+ var_1&0xff+ var_2&0xff00。
黑客无法控制 target、var_1 或 var_2。
但漏洞利用提供了从地址 target 读取的能力。
我们可以从 0x1000000 到 0x100FFFF 之间的任何地址读取,这在某些情况下可能有也可能没有用。
例如2: 假设一个漏洞利用提供了从匹配特定模式的地址读取一个 QWORD 并将其内容放入其安全令牌值的能力。假设我们之前已经获得了 System.exe 的安全令牌值。 我们如何利用该漏洞来提升权限?
read_addr=0x1000000+alfa&0xFFFF00
我们无法控制 alfa 参数。
题外话但非常重要:内核可以访问当前正在运行内核代码的进程对应的用户空间。
我们可以从哪些地址读取?嗯,0x1000000, 0x1000100(alfa=1), 0x1000200(alfa=2), ..., 0x1FFFF00(alfa=ffff00)。
为了确保漏洞利用成功,程序员应该执行以下操作:
memory=virtualalloc(dest=0x1000000,size=0x1000000,....)for (i=0x1000000;i<0x2000000;i+=0x100) ((QWORD*)i)[0]=system_token_value;这实际上就是内存喷射。按照特定模式操作内存,该模式匹配漏洞利用产生的概率表达式的所有可能值。
在我们在情况下,这个工作需要一个条件:内存可以在地址 0x1000000 处分配。这是一个限制,在真实的漏洞利用中我们也必须处理。
在澄清了一些技术方面后,现在需要对 CLFS 有一个基本了解。微软链接。CLFS 用于应用程序日志、数据库日志、事务等。
漏洞利用通常需要特定的内存布局才能工作。布局的形式由漏洞利用时程序内部的变量值决定。
本教程不会详细解释有漏洞的代码或日志文件系统的格式。它将提供对产生漏洞利用的过程的基本理解。
日志文件是一种特殊类型的文件,具有特定格式,可以通过 CLSF 驱动程序和 DLL API 进行交互。在这个系统中还有日志容器的概念。日志容器也是日志文件,但在内存中链接到主日志文件(它们被添加到的日志文件)。
这种结构的工作方式如下:
此操作强制在主日志文件内分配新的内存空间,并将修改主日志文件内存布局中的不同对象。内存布局的修改在概念上看起来像这样: