作者: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() 函数的地址。

然后我们调用 FindKernelModulesBase() 函数,该函数将使用 NtquerySystemInformation()(这次使用 SystemModuleInformation 参数)来查找这两个相同模块的内核基址,返回所有模块的信息。

这样,我们可以计算每个函数的偏移量,然后在内核中获取它们。

调用 pipeArbitraryWrite() 函数两次,有一个标志在初始调用时为零,当第二次调用时该标志值为 1,它将更改堆喷射的值。

在第一次调用中,内存地址 0x5000000 处存放了以下值

记住,这个值除了分配在那个方向外,还存储在我们的堆喷射中。

这就是第一次调用后内存的状态,正如我们所说,地址大约在 0x5000000

而在从内存 0x10000 开始的堆喷射中,每隔 0x10 字节存储指向 AttributeValueSize 的指针,此外还有指向 0x5000000 的指针。

以下序列将触发漏洞:

再次对精心构造的文件和另一个随机名称的文件调用 CreateLogFile()。
然后,使用这些文件的句柄调用 AddLogContainer()。

调用 NtSetinformationFile(),并关闭句柄,从而破坏指针(后续会解释)

堆喷射在此处防止蓝屏发生:

在此处设置断点,我们可以看到指针被破坏并指向我们的堆喷射,从而我们可以控制 vtable 的下两个函数调用。


RAX 取值 0x5000000,并首先跳转到位于 0x5000000+18 的函数,然后跳转到 0x5000000+8。


因此,首先跳转到 fnClfsEarlierLsn(),然后跳转到 fnSeSetAccessStateGenericMapping()。
我们从断点处跟踪,发现它到达了 CLFS!ClfsEarlierLsn()。

此函数被调用是因为它返回后,会将 EDX 设置为 0xFFFFFFFF

在地址 0xFFFFFFFF 处,我们存储了 SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 的结果

正如我们所提到的,当从 CLFS!ClfsEarlierLsn() 返回时,RDX 值为 0x00000000FFFFFFFF

我们到达第二个函数 nt!SeSetAccessStateGenericMapping()

这个函数很有用,因为 RCX 指向我们的堆喷射,而 RDX 值是 0xFFFFFFFF,其内容由我们控制。


RCX+0x48 的内容是指向 AttributeValueSize 的指针,该指针存储在 v30.Pointer+24 中。



该指向 AttributeValueSize 的指针值被移动到 RAX,然后读取地址 0xFFFFFFFF 处的内容,其中存储了 SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 的地址。

然后,它在 RAX+8 处覆盖下一个字段,即 AttributeValue()。


当然,正常情况下 AttributeValue 会在内核中指向我们添加的属性。

现在,我们将它覆盖为一个指向系统 _EPROCESS & 0xFFFFFFFFFFFFFFF00 结果的指针。
这意味着,当我们再次调用 NtFsControlFile() 函数(这次使用 0x110038 参数读取属性)时,它不会返回 AttributeValue 指针所指向的 “A”,而是从 _EPROCESS & 0xFFFFFFFFFFFFFFFFF000 地址读取请求的字节数,并在输出缓冲区中返回,这样我们就可以在第一次调用中获取到 SYSTEM TOKEN 的值。

v9b 是 Output Buffer 的起始地址,其中复制了 System EPROCESS & 0xFFFFFFFFFFFFFFF000 的结果。
他在此基础上加上 v14(即 System EPROCESS 的最后 3 个字节),然后加上 0x4b8(这是此 Windows 11 版本中 Token 的偏移量),然后找到该地址的内容,其中将保存 System Token 的值。



请记住,最后 4 位已被更改,这并不重要,因此值仍然匹配。
在第二次调用中,标志的值为 1,因为它在第一次调用结束时递增了。

此处我们看到值存储的顺序

地址 0xFFFFFFFF 中存放了我们刚刚找到的 System Process Token 的值。


而在堆喷射中,存放的是我进程的 Token 地址减去 8 的值。这个值加八将用作目标地址,请记住,写入操作是在 RAX+8 指向的地址上进行的。


在起始地址为 0x5000000 的内存中

我们还看到它使用了其他容器的名称,因为前一个容器正被系统进程使用,无法再次打开或删除。

然后,漏洞第二次以与第一次相同的方式被触发。

它再次到达 CLFS!ClfsEarlierLsn()。

将 RDX 设置为 0xFFFFFFFF

然后到达 nt!SeSetAccessStateGenericMapping()

读取我进程的 Token 地址减 8 的值,该地址即将被写入。

然后读取 SYSTEM TOKEN

并将 System Token 写入我进程的 Token 地址(它加上 8)

这样,我的进程就拥有了 System Token

写入令牌后,我们启动一个进程以检查权限,在此例中,我们启动 Notepad.exe



请注意,此 PoC 仅适用于 Windows 11,在 Windows 10 上会导致蓝屏,因此需要进行一些修改才能正常工作,本文不对此进行解释。
分析结构体
结构体以及关于 CLFS 文件格式的大部分文档,我们取自 IONESCU 关于 CLFS 内部结构 的优秀工作。
我们可以看到,在函数 ClfsBaseFilePersisted::LoadContainerQ 中新增了一个检查。

进行加法操作的值属于 _CLFS_BASE_RECORD_HEADER 结构体。

请注意,Base Block 从文件的偏移量 0x800 开始,到偏移量 0x71FF 结束,前 0x70 字节对应 Log Block Header
作为一种良好实践,我们可以在 IDA 中添加 _CLF_LOG_BLOCK_HEADER 结构
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
然后我们有基记录头(_CLFS_BASE_RECORD_HEADER),它从文件起始偏移量 0x870 处开始,长度为 0x1338 字节。

如果你想将其导入 IDA,必须先添加以下类型和缺失的结构
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
现在可以添加了:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

包含这些结构后,我们注意到它执行了 cbSymbolZone 与 _CLFS_BASE_RECORD_HEADER 结束地址之间的加法运算。(起始 + 1338h)
请记住,在精心构造的日志文件中,cbSymbolZone 已从 0x000000F8 修改为 0x0001114B。
(文件偏移量 0x1b98)
0x800(基块起始偏移量)+ 0x70(logBlockHeader)+ 0x1328(cbSymbolZone)
0x800+0x70+0x1328 = 0x1b98
在 MyLog.blf 文件上精心构造的 cbSymbolZone:


由于补丁位于 CClfsBaseFilePersisted::LoadContainerQ 函数中,我们必须查看 CClfsBaseFilePersisted 对象。
在 CLFS!CClfsBaseFilePersisted::LoadContainerQ 设置断点,当 CreateLogFile 被调用并传入精心构造文件的句柄时,它将中断。

调用 CClfsBaseFile::GetBaseLogRecord 函数以获取基日志记录(_CLFS_BASE_RECORD_HEADER)的地址

RAX 将指向 _CLFS_BASE_RECORD_HEADER 地址

注意内存中的 _CLFS_BASE_RECORD_HEADER 结构和 cbSymbolZone 字段 0x1328
字节向前


r14 存储对应“this”的结构,即 CClfsBaseFilePersisted,因为它是函数 CClfsBaseFilePersisted::LoadContainerQ 的 this。

内存中的 CClfsBaseFilePersisted 结构:

所以,让我们创建一个长度为 0x21c0 的结构,以便在我们逆向分析时填充其字段(这是一个未文档化的结构),我们将其命名为 struct_CClfsBaseFilePersisted

在函数 CClfsBaseFile::GetBaseLogRecord() 内部,获取指向 _CLFS_BASE_RECORD_HEADER 的指针。并且我们知道该函数中的“this”是结构:struct_CClfsBaseFilePersisted。

读取两个字段(偏移量 0x28 和 0x30)

偏移量 0x28 是一个 word,值为 6,因此我们在结构中将其类型更改为 word。



目前,我们将其重命名为常量 6(const_6)


根据文档,6 应该是块的数量 CLFS_METADATA_BLOCK_COUNT。该字段可能指代此值。
而该指针位于偏移量 0x30 处。

注意,此处显示的大小包括长度为 0x10 的头部


当 ExAllocatePoolWithTag 函数被调用时,会请求一些字节,但不包括头部,因此调用时将请求 0x90 字节(0xa0 – 0x10)。
通过文本 +30h] 搜索写入偏移量 0x30 的指令时,我们找到了一长串结果,但通过 CClfsBaseFilePersisted 对象类型过滤列表后,结果很少,我们立即找到了分配该大小和相同标签的位置。(提示: Create 和 Initialize 函数名称总是首先查看的)


由于我们仍然不知道名称,我们将其命名为 pool_0x90,这是另一个未文档化的结构,我们将创建一个该大小的结构。


内存中的 pool_0x90 在其自身偏移量 0x30 处有另一个指针。

这个另一个指针指向文件中的基块(基块起始于偏移量 0x800)


图片取自 Zscaler 博客文章:

该分配很大,因为它包含了整个基块。



因此,我们将创建一个大小为 0x7a00 的新结构,并命名为 BASE_BLOCK

前 70 个字节我们已经知道对应 _CLFS_LOG_BLOCK_HEADER,接下来的 0x1338 字节对应 _CLFS_BASE_RECORD_HEADER。

因此,将基块的起始地址与到下一条记录的偏移量(即 0x70)相加,我们就得到了 _CLFS_BASE_RECORD_HEADER

内存中的 _CLFS_BASE_RECORD_HEADER。

查看同一 CClfsBaseFilePersisted 对象的其他方法,在 CClfsBaseFilePersisted::AddContainer 中,同样通过 CClfsBaseFile::GetBaseLogRecord 获取 _CLFS_BASE_RECORD_HEADER 的地址。

接下来,使用 cbOffset 调用 CClfsBaseFile::OffsetToAddr,获取 _CLFS_CONTAINER_CONTEXT 的地址,并将 cboffset 存储在 rgbcontainers 数组中,该数组位于 _CLFS_BASE_RECORD_HEADER 的偏移量 0x328 处。

CClfsBaseFile::OffsetToAddr 函数用于从偏移量查找结构地址

此时,将存储在 0x328 处的容器偏移量仍为 0,因为我们尚未添加容器。

PoC 调用了两次 CreateLogFile,第一次使用畸形文件 MyLog.blf,第二次使用正常的 MyLogxxx.blf 文件,因此我们必须在上述所有位置两次停止调试,并在记事本中记录两个文件上述结构的地址。

让我们快进到 CLFS!CClfsLogFcbPhysical::AllocContainer,在其上设置断点并运行到那里。
当 POC 到达 AddLogContainer() 时,我们停在断点处。

我们还在 CClfsBaseFilePersisted::AddContainer+176 处设置一个断点,之前我们看到在那里会找到 _CLFS_CONTAINER_CONTEXT 结构的偏移量和指针。


当调试器中断时,我们可以看到偏移量为 0x1468。

在 RAX 中,将返回 _CLFS_CONTAINER_CONTEXT 结构的地址。

该结构仍然是空的,因为尚未添加容器。

注意,我们写入畸形文件偏移 0x868 处的 SignatureOffset=0x50 值,减去 0x800(基块起始地址),将位于 _CLFS_LOG_BLOCK_HEADER 结构的偏移量 0x68 处。


当 PoC 使用畸形文件调用 AddLogContainer() 函数时,在 _CLFS_LOG_BLOCK_HEADER 的偏移量 0x68 处,我们写入的 0x50 值现在变成了内存中的 0xFFFF0050。

在某个时刻,该值被程序修改了,为了查看何时发生,在下一次执行中,我们将设置一个写入内存断点。
该偏移量存储在 r15 + 0x328 处(r15 指向 _CLFS_BASE_RECORD_HEADER 结构)


RBX 存储偏移量 0x1468。

因此,在基块地址 + 0x70 + 我们发现的偏移量 0x1468 处,将存在 CLFS_CONTAINER_CONTEXT 容器的地址。

在 CLFS_CONTAINER_CONTEXT 结构的偏移量 0x18 处,将是存储在那里的 pContainer 指针,我们可以设置写入断点并查看它何时被写入。


这是我们必须损坏的指针,因为在存在漏洞的函数中,它首先读取 CLFS_CONTAINER_CONTEXT,然后将其移动到 r15,接着读取 r15+18 的值,这正是我们刚刚设置写入断点的指针。


它将 pContainer 存储在 struct_CClfsBaseFilePersisted 结构的偏移量 0x1c0 处。

在它停止多次之后,我们到达了它被损坏的时刻。指针地址的高位已从 FFs 改为零。

当第二次对畸形文件调用 AddLogContainer() 时,就会发生这种情况,前一个 MyLogxxx 的指针被损坏了。
问题发生是因为 SignaturesOffset(本应为 0x50)现在是 0xFFFF0050,因此允许在随后的 memset 中进行越界写入。


memset() 函数将损坏下面的 _CLFS_CONTAINER_CONTEXT 结构,该结构对应于 MyLogxxx 文件,因为在创建时,它们彼此相距 0x11000 字节。
这样,它精确计算出写入下一个结构的位置,并将指针的高位清零,使其指向用户堆,我们已在该堆上创建了 HeapSpray。
畸形文件的基块结构正好位于 MyLogxxx 文件之前 0x11000 处。
畸形:

MyLogxxx


RCX 小于 RDX,因为加上了 0xFFFF0050,而不是应有的 0x50。

我们到达了 memset() 函数,它将 0xb0 字节设置为零,RCX 指向 MyLogxxx 文件的 CLFS_CONTAINER_CONTEXT 结构,具体指向 pContainer 的高五个字节。

这个指针将通过覆盖前几个字节而被损坏:

剩下的指向一个我们之前通过 HeapSpray 控制的内存地址


然后,MyLogxxx 文件的句柄将被关闭,并到达 CClfsBaseFilePersisted::RemoveContainer,漏洞最终被触发。

现在我们掌握了更多信息,我们注意到这里读取了 Base_Block.LOG_BLOCK_HEADER.SignaturesOffset 和 Base_Block.LOG_BLOCK_HEADER.TotalSectorCount
在补丁的第一部分中,SignaturesOffset 不应大于 0x7a00,在我们的例子中它原本是 0x50,如果它以大于 0x7a00 的值到达,它将把我们踢出。

在已打补丁的机器上运行 PoC,它将 0x50 与 0x7a00 比较,由于 0x50 较小,程序继续执行。

在接下来的块中,将畸形的 cbSymbolZone 加到 _CLFS_BASE_RECORD_HEADER 的结束地址上,并将这个和存储在 result_1 中。

然后,将基块的地址与 SignatureOffset 值相加,在正常文件中该值为 0x7980。

base_block 的最大地址为 0x7a00,现在 SymbolZone 最多允许在限制前 0x80 字节。
它将存储在 result_2 中,也就是说,这将是 SymbolZone 在基块内的最大限制,然后它比较两个结果,如果第一个大于第二个,则意味着越界。


显然第一个成员将大于第二个,并且程序不会继续,因为 cbSymbolZone + _CLFS_BASE_RECORD_HEADER 的结束地址 的和超过了限制(即 result_2),导致“越界”。

我们最后需要弄清楚的是,SignatureOffset 的值 0x50 是如何变成 0xFFFF0050 的。那么,让我们重新开始,重启并在 CLFS!CClfsBaseFilePersisted::LoadContainerQ 处停下,此时内存中的值尚未改变,仍为 0x50。
在 SignatureOffset 的偏移量 0x68 处设置一个访问断点。

经过几次停止后,我们在 ClfsEncodeBlockPrivate 中检测到修改值的正确时刻。

这个函数没有被打补丁,因此这可能是由 0x50 的低值以及其他被操纵的值引起的行为。
在被精心构造的值中,我们可以看到一个名为 ccoffsetArray 的值,它在 _CLFS_BASE_RECORD_HEADER 结构中的名称为 rgClients,表示指向 Client Context 对象的偏移量数组。
rgClients 字段位于偏移量 0x138(_CLFS_BASE_RECORD_HEADER 结构的 0x9a8-0x800-0x70)处。


在 PoC 中,该值被篡改,指向一个伪造的客户端上下文对象,称为 FakeClientContext


这是 Client Context 结构 _CLFS_CLIENT_CONTEXT
struct _CLFS_CLIENT_CONTEXT { CLFS_NODE_ID cidNode; CLFS_CLIENT_ID cidClient; USHORT fAttributes; ULONG cbFlushThreshold; ULONG cShadowSectors; ULONGLONG cbUndoCommitment; LARGE_INTEGER llCreateTime; LARGE_INTEGER llAccessTime; LARGE_INTEGER llWriteTime; CLFS_LSN lsnOwnerPage; CLFS_LSN lsnArchiveTail; CLFS_LSN lsnBase; CLFS_LSN lsnLast; CLFS_LSN lsnRestart; CLFS_LSN lsnPhysicalBase; CLFS_LSN lsnUnused1; CLFS_LSN lsnUnused2; CLFS_LOG_STATE eState; union { HANDLE hSecurityContext; ULONGLONG ullAlignment; }; };
eState 值位于结构起始偏移量 0x78 处,在精心构造的文件中为 0x23a0+0x78。


该值显示日志的状态。
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
该值被设置为 CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20
另一个被篡改的值是 fAttributes,它对应于与基本日志文件关联的 FILE_ATTRIBUTE 标志集(例如系统和隐藏)。


由于该字段从 0xa 处开始一个字节,并跨越两个字节,因此 fAttributes 的值为 0x100。


最后,有一个 blocknameoffset 值,它指向偏移量 0x1bb8,我的意思是,加上 0x78 和 0x800 就指向文件的偏移量 0x2428。


请注意,指向 Client Context 的偏移量是 0x1b30

因此,Client Context 位于偏移量 0x23a0。


而在它前面 0x10 处,是对应于 blocknameoffset 的值。


这将指向包含名称的字符串
最后一个是 blockattributeoffset,它在 0x2394 处,位于 Client Context 之前 0xC 的位置。

最后这两个值属于一个位于 Client Context 之前、长度为 0x30 字节的结构,称为 _CLFSHASHSYM
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


它们位于 _CLFSHASHSYM 结构起始偏移量 0x20 和 0x24 字节处,因此在 _CLFSHASHSYM 结构中,POC 中称为 blockNameOffset 的值是 cbSymName 字段,而 blockAttributteoffset 是 cbOffset 字段。


这些就是被篡改的值,现在我们需要看看它们如何影响将我们的 SignaturesOffset 从 0x50 值更改为 0xFFFF0050。
让我们来看看 CClfsBaseFile::AcquireClientContext() 函数,它应该返回客户端上下文。

它调用 CClfsBaseFile::GetSymbol,第四个参数将是 _CLFS_CLIENT_CONTEXT **,用于存储指向 Client Context 的指针。

在 CClfsBaseFile::GetSymbol 函数内部,我们将被篡改的 ccoffsetArray 偏移量传递给 CClfsBaseFile::OffsetToAddr,并获取客户端上下文的地址。让我们在那里设置一个断点,这样当调用使用 CreatelogFile 创建的文件时,它就会停止。

它停在带有精心构造的 ccoffsetArray 参数的地方。


CClfsBaseFile::OffsetToAddr 函数返回假的 Client Context

并且检查 cbOffset 的值是否不为零,因为在 RAX 中的 _CLFS_CLIENT_CONTEXT 结构之前发现了 0xC。


然后它将 cbOffset 与 ccoffsetArray(位于 RSI 中)进行比较,它们必须相等,否则我们会得到错误。

它还检查 cbSymName 必须等于 cbOffset+0x88,否则我们也会得到错误。

最后,它比较 cidClient 字节是否为零

如果所有这些检查都成功,则 client context 将被保存。

函数 r14 的输出指向 Client Context

从 CClfsLogFcbPhysical::Initialize 退出时,我们将拥有 CLFS_CLIENT_CONTEXT 的地址。

现在它读取 fAttributes 的值(0x100)

这个函数属于类 CClfsLogFcbPhysical


它被分配在这里,大小为 0x15d0,标记为 “ClfC”

让我们创建一个结构来存储我们正在逆向的内容,我们称之为:struct_CClfsLogFcbPhysical.

注意,在 0x2b0 处,它保存了 CClfsBaseFilePersisted 结构的地址。

在结构中保存了许多值之后,它进入一个重要的部分,它用 0x20 测试 eState。


由于精心构造的值是 0x20,测试将返回 1。


我们看到在构造函数中,vtable 是

它将检查文件是否是多路复用的。

因此,它沿着期望的路径前进,到达 CClfsLogFcbPhysical::ResetLog.


几个字段被初始化为零,除了一个被初始化为 0xFFFFFFFF00000000。

这里检索 Client Context

它存储值 0xFFFFFFFF00000000。



它写入 0xFFFFFFFF 到偏移量 0x5c,这是 CLFS_LSN lsnRestart.ullOffset 的高位部分



现在执行 ClfsEncodeBlockPrivate() 函数,该函数负责将 0x50 覆盖为 0xFFFF0050,正如我们之前所见。
它在那里读取 SignatureOffset = 0x50 的值,该值仍然如我们放在精心构造的文件中一样,并将其添加到 CLFS_LOG_BLOCK_HEADER 的起始地址。

这是一个正在写入 2 字节的循环,就像 SignatureOffset 没有指向一个正确的值一样,在正常文件中该值是一个高值,例如 0x3f8,这使得它写入更靠前的位置,而这里它将写入相同的 CLFS_LOG_BLOCK_HEADER
其想法是更改写入目标,以试图破坏 SignatureOffset 值。
正常文件

此时,它将开始循环并写入两个字节。

计数器必须达到值 0x3d 才能退出循环。

RCX 从 0x200 开始递增,我们已经处于第三个周期,其值为 0x600

在第 0xe 次迭代中,RCX 为 0x1a00


那就是他写入 0xFFFFFFFF000000 的地方。


它正在读取最后两个字节 FFFF

然后将其复制到 R8 中


正如我们所见,这个值至关重要,因为它允许绕过检查并越界写入,以破坏之后 memset() 函数中文件的 pContainer 指针,并在顶部写入零,使其指向我们控制的内存(HeapSpray)。
在 CClfsBaseFilePersisted::AllocSymbol 中,相同的加法将获得 memset 的目标地址,即 cbSymbolZone + CLFS_BASE_RECORD_HEADER 的最终地址,在此之前将其与 Base_block + 0xFFFF0050 进行比较,因此等式两边都有被破坏的值。
CbSymbolZone= 0x1114B
这是一个被破坏的值,加上 CLFS_BASE_RECORD_HEADER 的最终地址将使其越界写入,而比较的另一项应该是 Base Block + SignatureOffset 的地址,SignatureOffset =0xFFFF0050 允许此检查通过,并在 memset() 中越界写入,将指针的顶部归零,该指针将保持指向我们的 HeapSpray。

因为 RCX 小于 RDX。

正如我们之前所见。(值可能有所不同,因为它们属于之前的执行)
它将破坏指针,将最高字节设置为 0

使其指向我们通过 HeapSpray 控制的内存区域


因此,当漏洞被触发时,我们到达 CClfsBaseFilePersisted::RemoveContainer

那里将存在已经被破坏的指针,并且可以像我们之前看到的那样进行利用。

此时我们已成功利用漏洞,它导致我们可以控制允许读取 SYSTEM 令牌并将其写入我们自己的进程以实现本地权限提升的函数。
我们希望您觉得它有用,如果有任何疑问,可以通过 [email protected] 和 [email protected] 联系我们。
享受吧!