作者:Ricardo Narvaja 和 Daniel Kazimirow (Solid)
仅用于演示目的。完整的漏洞利用适用于易受攻击的 Windows 11 21H2 系统。
基于先前 Zscaler 发布的信息 的功能性 PoC。
查看相关文章 理解 CVE-2022-37969 Windows 通用日志文件系统驱动程序本地权限提升。
利用过程详解:
此处使用的场景是 Windows 11 21H2 (OS Build 22000.918) clfs.sys v10.0.22000.918
第一步是使用 CreateLogFile() 函数在公共文件夹 (%public%) 中创建一个名为 MyLog.blf 的文件:



然后,它使用循环创建多个名称随机的日志文件。
在循环内部,它调用我们的 getBigPoolInfo() 函数:

它调用 NtQuerySystemInformation(),第一个参数为 0x42 (十进制 66),该调用将在 v5 中返回关于 bigpool 中 raid 的信息,其结构体类型为 SYSTEM_BIGPOOL_INFORMATION。

我们需要调用此函数两次。第一次会返回错误,但会给出正确的缓冲区大小,以便第二次调用时获取所需信息。

v5 将接收 SYSTEM_BIG_POOL_INFORMATION 结构体的信息。

bigpool 中分配的数量存储在第一个字段 Count 中,第二个字段是 SYSTEM_BIGPOOL_ENTRY 结构体的数组。

然后,我们遍历所有结构体,查找标签为 “Clfs” 且大小为 0x7a00 的项。

它将这些结构体的第一个字段(VirtualAddress)存储在一个名为 kernelAddrArray 的数组中,这些结构体具有 CLFS 标签和大小 0x7a00。从现在起,同时满足这两个条件的池被称为 “正确池”。

除了将每个 正确池 存储在数组中之外,它还将最后找到的 正确池 存储在函数参数 a2 指向的内容中。

这样,a2 始终指向最后创建的具有 CLFS 标签和大小 0x7a00 的 正确池。
变量 v26 始终存储前一个找到的 正确池,因为它在调用 getBigPoolinfo() 之前等于 v24 (v26=v24),但退出此调用后,v24 会更新为最后找到的 正确池,而 v26 保持为前一个找到的 正确池。

然后,它计算两个地址的差值,如果结果为负,则交换操作数以确保结果始终为正。

这样,v32 将存储最后两个 正确池 的 VirtualAddress 之间的差值。
然后,它执行类似的操作,此例中 v23 初始为零,因此第一次执行 v23=v32。

下次循环时,v23 仍保持相同值且不为零,因此跳出并转到此处。

V32 包含最新的差值,v23 包含前一个差值,如果它们相等,则跳出并递增一个计数器,否则将计数器重置为零。
其目的是找到 6 个连续的 CLFS 标签和大小 0x7a00 的池,它们之间的差值相等,且该差值应为 0x11000。我们将会看到,当找到 6 个(因为从零开始)连续且距离相等的池时,它会给出它们之间的差值。


在这里我们看到,它找到了 6 个连续的池,并退出了创建日志文件的循环。
在 “public” 文件夹中,我们可以看到创建的文件

我们的 craftFile() 函数打开原始文件 (MyLog.blf) 并修改它以触发漏洞。

修改文件后,必须更改 CRC32,否则会出现文件损坏错误。
该值位于文件的偏移量 0x80C 处。

接下来,它执行堆喷射,使用 VirtualAlloc() 函数在任意地址 0x10000 和 0x5000000 上分配内存,并在第二个分配 (0x10000) 中,每 0x10 字节存储值 0x5000000。

它使用 CreatePipe() 创建一个匿名管道,并调用 NtFsControlFile(),使用 0x11003c 作为参数来添加一个属性,之后可以再次调用此函数,使用 0x110038 参数来读取该属性。
关于此方法的更多详细信息,请参见 此处

此处我们看到输入缓冲区就是我们要添加的属性,如果再次调用 NtFsControlFile() 并传入参数 0x110038,输出应该返回相同的这个属性。

在池中搜索创建的属性的标签(NpAt)


当找到它时,将该池的 VirtualAddress 保存在 v30.Pointer 中。
V30.pointer+24 指向内核池中的 AttributeValueSize,并将其保存到我们之前进行的堆喷射之一中。

其想法是写入该内核地址+8,以覆盖 AttributeValue。


PipeAttribute 结构体的第一个字段是一个大小为 16 字节的 LIST_ENTRY,接着是一个指向属性名称的指针,大小为 8 字节,然后在偏移量 0x18 (24 十进制) 处是 AttributeValueSize 字段,这就是我们存储在堆喷射中的那个字段。
之后,我们在用户模式下加载 CLFS.sys 和 ntoskrnl,并使用 GetProcAddress() 找到 ClfsEarlierLsn() 和 SeSetAccessStateGenericMapping() 函数的地址。
