自2022年2月起,有报道称出现了一种新的勒索软件,该软件似乎利用了Windows 0-day漏洞,这是根据Trend Micro的研究得出的。
关于这种勒索软件的更多信息可以在这个链接中找到。
根据卡巴斯基的分析,Nokoyawa勒索软件组织自2022年6月以来,一直使用其他针对通用日志文件系统(CLFS)驱动程序的漏洞,这些漏洞具有相似但不同的特征,全部与同一个漏洞开发者有关。
2023年4月,当微软发布补丁时,分配了CVE-2023-28252。
此前,在2022年,我们研究了同一组件中的一个类似漏洞,并记录在这篇博客文章中。
为了进行分析,有必要了解***.blf文件格式,该格式由易受攻击的通用日志文件系统*驱动程序CLFS.sys**处理,该驱动程序位于system32中的驱动程序文件夹内。
有关此文件类型的更多信息,请参见以下链接:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
本分析针对Windows 11 21H2,clfs.sys版本10.0.22000.1574,尽管它也适用于Windows 10 21H2、Windows 10 22H2、Windows 11 22H2和Windows Server 2022。
在之前的Windows版本中,需要调整一些值,否则会导致蓝屏死机(BSOD)。
您可以像这样检查驱动程序版本
当漏洞公布时,2023年4月我开始与Esteban Kazimirow一起对CLFS.sys驱动程序进行逆向工程,尽管在这种情况下,仅仅分析补丁很难推断出漏洞在哪里以及如何触发它,因为利用非常复杂。
后来,出现了一篇博客文章,其作者从一个恶意软件样本中展示了HexRays反编译的一些代码片段,以及一些指导如何应对利用的信息。
显然,提供的信息并不完整,但如果没有这些帮助,就很难构建出PoC,进而构建出可用的利用程序。
为了更容易理解,我们将首先解释如何构建PoC,然后进行漏洞分析。
这篇博客文章包含两个部分:
构建PoC:
1-获取我们利用所需的内核地址
2-准备创建.blf文件的路径:
3-使用CreateLogFile()函数创建“触发器blf”文件
4-制作“触发器blf”文件
5-获取触发器blf的BASE BLOCK的内核地址
6-使用触发器blf的句柄调用AddLogContainer
7-准备喷洒blf文件
8-准备执行喷洒的内存
9-触发漏洞
调试:
1-检查内存喷洒
2-查看触发器blf的RecordOffset[12]
3-查看喷洒blf文件中的iFlushBlock值
4-为什么它从BLOCK 1 SHADOW读取,而不是BLOCK 0 CONTROL?
5-为什么在blf喷洒文件中校验和等于零?
6-完成利用。
7-真正的补丁
我将创建一个名为InitEnvironment的函数来获取一些必要的内核地址。
获取我的进程的EPROCESS地址并将其存储在g_EProcessAddress变量中,然后获取SYSTEM进程的EPROCESS地址并存储在system_EPROCESS中,接着获取我的进程的主线程的EHTREAD地址并存储在g_EThreadAddress中,最后获取PREVIOUS MODE的地址,在该PoC版本中不会使用。

这种方法众所周知,GetObjectKernelAddress函数调用NtQuerySystemInformation两次,第一个参数为SystemExtendedHandleInformation,第一次调用时传递了一个错误的大小并返回错误,但同时也返回了正确的尺寸,用于第二次调用,从而获取所有句柄的信息,然后在循环中遍历每个句柄的信息,在正确的handleinfo的Object字段中获取内核中搜索的地址。

我还需要由CLFS.sys导出的以下函数的内核地址:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
以及NTOSKRNL.exe导出的函数:
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
为了获取这些地址,使用类似于获取两个模块内核基址的方法,通过两次调用NtQuerySystemInformation,但这次第一个参数将是SYSTEM_INFORMATION_CLASS(在PoC中,我们使用FindKernelModulesBase函数来实现此目的)。
然后,它将CLFS.sys和NTOSKRNL.exe作为用户模式下的普通模块加载,调用LoadLibrary,使用GetProcAddress获取用户模式下的地址,然后从每个地址中减去镜像基址,从而获得函数的偏移量,最后将每个偏移量添加到相应的内核基址,从而获得所有必要函数的内核地址。

我创建一个名为createInitialTriggerBlfFile的函数,它将生成并写入一个**.blf文件**。
在CreateLogFile中用作参数的路径与普通路径不同,例如,要打开位于C:\Users\Public文件夹中的文件1280.blf,我们必须设置路径LOG:C:\Users\Public\1280.。这将被保存在stored_name_CreateLog变量中。
我使用wsprintfW() 来实现这一点,因为stored_env存储了路径C:\Users\Public,该路径之前从环境变量中获取。我将在此字符串前面加上字符串LOG:,并在末尾加上一个随机名称,不带.blf扩展名。

这将是我想称为“触发器blf”的初始文件的路径。当然,我还必须保存同一文件的普通路径,不带前面的LOG:,并带有BLF扩展名,以便使用CreateFile()、WriteFIle() 像任何其他文件一样打开和修改它,这个路径将是,例如:C:\Users\Public\1280.blf,并将存储在stored_name_fopen变量中**。**

当然,这两个路径对应的是同一个文件,我必须根据情况使用其中之一。
CreateLogFile函数的功能与CreateFile() 非常相似(创建新文件或打开现有文件并获取其句柄),甚至一些参数也类似,但CreateLogFile() 仅适用于blf文件。
此外,当打开现有文件时,它会验证格式是否正确,即使每个块都有一个校验和,如果校验和不正确,它将返回错误。
我将创建两种类型的BLF文件:
Trigger blf(触发器blf)
Spray blf(喷洒blf)
两者都是blf文件,但以不同的方式修改。
通过这种方式,PoC首先使用CreateLogFile创建“触发器blf”文件,使用的路径例如:LOG:C:\Users\Public\1280,我之前已设置好该路径,并存储在stored_name_CreateLog变量中。
第五个参数fCreateDisposition,与CreateFileA() 一样,可以取以下值:

在这种情况下,我将使用OPEN_ALWAYS参数,因此如果文件不存在则创建,如果存在则打开。由于该文件尚不存在,它将使用随机名称创建。
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile() 将创建我们的“触发器blf”文件,包含其6个块及其相应的校验和,并返回句柄,该句柄将存储在logFile变量中。

每个块将从左侧列显示的偏移量开始,有一个大小为0x70字节的头部。
因此,例如,CONTROL BLOCK的头部从偏移量0x0到0x70。

所有块的头部都具有相同的结构,称为**_CLFS_LOG_BLOCK_HEADER**。
这是头部结构:

在头部的偏移量0xC处,我可以找到checksum,因此由于CONTROL BLOCK从偏移量0开始,校验和将在文件的偏移量0xC处,并且每个块的校验和将位于其块起始偏移量0xC处。

要修改触发器blf文件,我必须将其作为普通文件打开,可以使用CreateFileA或fopen,然后分别使用WriteFile或fwrite修改它,我在PoC的fun_prepare函数开头执行此操作。
请记住,普通路径存储在stored_name_fopen变量中,因此我使用它通过wfopen_s(它是支持Unicode字符串的fopen变体)打开文件。
在从fun_prepare调用的craftTriggerBlfFile函数中修改该文件。

然后,我调用fseek指向要更改的偏移量,然后使用fwrite修改文件。
对“触发器blf”文件所做的更改如下:
进行这些更改后,调用FixCRCFile计算新的校验和并修复前4个块的校验和。接下来的两个块没有任何更改,因此无需重新计算它们的校验和。

CLFS.sys驱动程序读取文件的六个块,并为了存储其内容,在内核池中进行分配。

有一个非常重要的结构,大小为0x90,在之前的博客文章关于CVE-2022-37969中,通过逆向工程,我发现了一些字段,并称其为pool_0x90。经过更多的逆向工程,现在我知道它的真正名称是m_rgBlocks,并且随着控制器分配内存以从文件中复制每个块的内容,它会在其中保存每个块的大小、起始偏移量以及存储的内核地址。

它有六个CLFS_METADATA_BLOCK,每个对应一个块,按块编号排列。
每个CLFS_METADATA_BLOCK结构长度为0x18字节。(0x18*6=0x90)
在偏移量0处有一个联合体,但至少在这个利用中,只使用了pbImage字段,因此简化后将是:
该结构的分配可以从CLFS.sys驱动程序的两个不同位置进行,具体取决于创建新文件还是打开现有文件。如果是创建新文件,驱动程序从CClfsBaseFilePersisted::CreateImage+28A分配0x90字节;如果是打开现有文件,则从CClfsBaseFilePersisted:ReadImage+6E分配。
之后,我将获取与触发器blf文件对应的块2的起始地址,称为BASE BLOCK,它从偏移量0x800开始,长度为0x7a00。

在fun_prepare函数内部,此地址将使用以下代码段在内核中找到。

首先,getBigPoolInfo函数在池中查找所有具有“Clfs”标签且大小为0x7a00的分配,然后将它们存储在一个数组中。
之后,它使用CreateLogFile并传递OPEN_EXISTING参数,再次打开先前修改的触发器blf文件,这将打开一个现有文件,从而分配其BASE BLOCK。
当再次调用getBigPoolInfo时,将有一个新的大小为0x7a00的“Clfs”池,并通过两次调用NtQuerySystemInformation来检索其地址。
触发器blf文件的BASE BLOCK地址存储在CLFS_kernelAddrArray变量中。

请注意,如果修改后的触发器blf文件没有正确的校验和,CreateLogFile() 函数将失败。
fun_prepare函数的最后一部分,使用触发器blf文件的句柄调用AddLogContainer api。

在PoC的最后一个函数中,称为to_trigger,将创建第二种类型的blf文件,
我将其命名为spray blf(喷洒blf)。
这种类型的文件将用于填充内存空间(喷洒),需要10个相同的这种类型,但最初只创建一个。
将创建三个数组来存储这些文件的随机名称:
stored_log_arrays: 存储十个新的.blf文件随机名称,这些文件将与CreateLogFile一起使用。
stored_container_arrays: 存储随机名称,以创建十个新的容器文件。
stored_fopen_arrays: 存储第一个数组(stored_log_arrays变量)的日志文件名,但使用其普通路径(不带“LOG:”字符串)和.blf扩展名。

在每次迭代中,使用CopyFileW复制blf文件,并分配存储在数组中的名称。
fun_trigger函数调用craftSprayBlfFile,其中对每个文件进行修改,FixCRCFile将修复CRC。

总结一下,我创建了10个类似的(喷洒blf)随机名称文件,进行了以下修改:

最后的修改是将整个块0(CONTROL BLOCK)复制到块1(CONTROL BLOCK SHADOW)

这些更改以及针对触发器blf文件进行的更改的效果将在后面的调试章节中解释。
其中一些更改是产生漏洞的,而另一些只是为了绕过驱动程序的检查。
此时,文件已创建并修改完毕,准备执行喷洒,然后当使用CreateLogFile打开它们时,它们将位于我们想要的内存区域中,稍后将展示。

在to_trigger函数中,创建了一个包含12个元素的数组,其中包含触发器blf文件的BASE BLOCK地址加0x30。
然后,在fun_pipeSpray函数中,使用管道喷洒填充内存,内部有一个循环,调用CreatePipe并创建作为第一个参数传递的管道数量,第二个参数是一个数组,用于存储所有创建的管道的句柄。

在一个循环中,它调用CreatePipe创建读写管道。
通过这种方式,首先创建0x5000个管道,然后再次调用创建另外0x4000个管道。
然后使用WriteFile向前5000个管道写入刚刚创建的包含触发器blf文件的BASE BLOCK + 0x30地址的数组。

现在已经在内存中创建了一个紧凑的块,它将从编号0x2000开始释放0x667个管道,直到0x2667,因为在内存中管道不是按照创建的顺序排列的,会发生的是在这个内存块中会有空闲空间。
请注意,管道的分配的用户大小为0x90字节,因此当释放时,我们将得到
它释放管道之间的0x90大小的内存空间。
然后循环调用CreateLogFile打开10个喷洒blf文件。
当CreateLogFile被调用来打开现有文件时,会为每个喷洒blf文件分配0x90字节的m_rgBlocks,因此这些分配将占据释放管道时留下的空隙,因为它们的大小相同。

然后重复该过程,向最后的0x4000个管道写入包含触发器blf的BASE BLOCK +0x30地址的数组。
所有这些操作创建了一个受控的内存空间,我将展示调试时的样子,但思路是每个喷洒blf文件的m_rgBlocks占据了被释放的0x90字节空隙。
然后,在最后部分,在一个while( 1 )循环中,通过调用AddLogContainer到喷洒blf文件来触发漏洞。
在此循环内触发漏洞:

该循环在找到系统令牌时将退出,使用 NtFsControlFile 函数读取管道属性。

然后使用 CreateLogFile,再次用最近找到的系统令牌覆盖我们进程的令牌,从而实现权限提升。

然后恢复一些值,关闭管道和 blf 文件的句柄,并以系统身份运行记事本,以验证我们已正确提升权限。

注意在 PUBLIC 文件夹中创建的 blf 文件。请记住,如果您想再次尝试,必须先删除创建的文件。有些文件会被锁定无法删除,但 PoC 仍会生效。

在我开始讨论对 trigger blf 和 spray blf 文件的更改效果以进行利用之前,我必须验证 spray blf 文件的 m_rgBlocks 是否位于内存分布中出现的空洞中,这是在执行管道喷射以及随后释放固定数量的管道之后。
当此过程结束时,应在 m_rgBlocks 的 0x90 字节下方放置一个管道,因此当使用 m_rgBlocks 时,将发生 越界,并且它将从下方的管道中读取数据。
PoC 有一个理想的位置来设置断点:

此时,spray blf 文件的打开已完成,AddLogContainer 函数尚未调用。
为了在用户模式下调试,我将使用 x64dbg,在内核模式下,使用带有 Windbg 插件的 IDA。

此时内存应该已经准备好,我可以查看分布情况。
我将暂停 IDA 以找到一个有趣的点来设置断点。
我将在 CClfsBaseFilePersisted::AddContainer 处设置一个断点,该函数从 AddLogContainer 调用,并且在开头,RCX 寄存器指向 CClfsBaseFilePersisted 结构,偏移量 0x30 处有一个指向 m_rgBlocks 的指针。

当断点命中时,我在调用堆栈上检查 AddLogContainer 是否正在从我的 PoC 调用。

RCX 寄存器指向:

第一个字段是指向虚函数表(CLFS! CClfsBaseFilePersisted::'vftable')的指针,在偏移量 0x30 处是指向 m_rgBlocks 的指针。

块 0、1、4 和 5 尚未保存 pbImage,而块 2(基本块)和 3(影子块)已经保存。
m_rgBlocks 表中的每个块都有其 cbOffset,即块在文件中的起始偏移量,cbImage 是块大小,eBlockType 是块类型。
如果喷射正确,在 m_rgBlocks 下方应该有一个管道,并且其中包含指向 trigger blf 的 BASE BLOCK + 0x30 的指针。

使用 windbg 的 "!pool" 命令显示内存分布:

每个 m_rgBlocks 都有一个“Clfs”标签,其大小为 0xa0,因为它是 0x90 的用户大小加上 0x10 的头部,而下方有一个带有“NpFr”标签的管道,其用户大小也是 0x90 + 0x10 头部。
由于分布并非精确科学,一些“Clfs”被连续放置,这是不可取的,但我正在处理的这个被正确放置在管道之后。
影响到的第一个更改是在 trigger blf 文件的偏移量 0x858 处所做的,其中存储了值 0x369。

BASE BLOCK 从文件的偏移量 0x800 处开始。

在 _CLFS_LOG_BLOCK_HEADER 中,偏移量为 0x800+0x58(从 BASE BLOCK 头部的 0x58 处开始)。

在偏移量 0x28 处,数组 RecordOffsets(DWORD)开始。
向前移动 0x30 字节,在偏移量 0x58 处(从开头算起为 0x828+0x30=0x858),是 RecordOffsets 的 字段 12。

我运行 PoC 到 CreateLogFile,如下图所示:


在进入 CreateLogFile 之前,我将在值 0x369 尚未被使用的位置设置一个断点。
在 CreateLogFile 打开现有文件的情况下,m_rgBlocks 结构在此处分配:
CClfsBaseFilePersisted::ReadImage+6E
因此,我将在 IDA 中在此处设置一个断点:

当断点触发时:

在 m_rgBlocks 中仍然有一些垃圾数据,因为它尚未初始化,但是一旦块 2 的 pbImage 被分配,地址将保存在偏移量 0x30 处,因为每个 CLFS_METADATA_BLOCK 内部的第一个字段是 pbImage。


现在我设置一个硬件写入断点:ba w1 ffffd003'7f5bea30
在初始化为零之后,它会在保存 pbImage 时停止。

分析表明它对应于 block0,因为它不考虑后面的常量 r14*8,即 0x30,结果实际上是写入 block 2 的 pbImage。

请注意,CClfsBaseFilePersisted::ReadMetadataBlock 用于分配任何块,使用作为参数传递的大小。

现在在基本块的 0x58 处设置一个 读/写 断点,以查看何时使用值 0x369。
ba r1 FFFF978A'16ECF000+0x58

当断点命中时,读取位于 RecordOffset[12] 的 0x369 值,将其添加到 r14 上的一个奇怪指针,并递增 RAX+r14 的内容。
在代码中向上几行,ESI 的值为 0x13,并乘以 0x18,这是 m_rgBlocks 中每个块的大小。
WINDBG>? 0x18*0x13
评估表达式:456 = 00000000'000001c8
如果我将 r8= 0x1c8(大于 0x90)的值加到 m_rgBlocks 的起始地址上,它将读取 越界。


在 m_rgBlocks 下方,是包含指向 BASE BLOCK + 0x30 的指针的管道,它读取这个被策略性地放置在管道内的指针。
代码中的当前位置是从主模块的 while(1) 语句调用的。
在 spray blf 文件中,我策略性地在偏移量 0x48a 处放置了值 0x13(iFlushBlock)。

在 spray blf 文件的偏移量 0x8a 处,是 BLOCK 0 的 iFlushBlock,其值为 4,而偏移量 0x48a 属于 BLOCK 1 的 iFlushBlock,其值为 0x13

现在我需要找出为什么它读取 BLOCK 1 的 iFlushBlock = 0x13 而不是 BLOCK 0 的 iFlushBlock = 4。
如果我回溯以找出 0x13 来自何处,我会看到调用堆栈上 WriteMetadataBlock 是从 CClfsBaseFilePersisted::ExtendMetadataBlock+416 调用的,在那里第二个 iFlushBlock 参数是 EDX=0x13,它来自 r9w。


在前面几行,调用了 CClfsBaseFile::GetControlRecord 来检索 BLOCK 0 的地址,可能问题就在这里,所以我将重新启动并在其上设置一个断点。
GetControlRecord 调用 CClfsBaseFile::AcquireMetadataBlock,它应该用 block 0 的地址填充 m_rgBlocks 表,当我单步执行此函数时,它获取了 block 1 的地址,因此问题发生在 CClfsBaseFile::AcquireMetadataBlock 内部。
通过将 0x8A 添加到检索到的地址,我可以确认 0x13 值(属于 BLOCK 1)存在。

我将重新启动并在那里设置一个断点:

CClfsBaseFile::GetControlRecord+27 调用 CClfsBaseFile::AcquireMetadataBlock

传递给 AcquireMetadataBlock 的第二个参数是 zero,它对应于 block 0,它将从文件中复制并存储其地址到 m_rgBlocks
。
在 _CLFS_METADATA_BLOCK_TYPE 块类型 枚举 中,它们的名称与我使用的不同,但它们是相同的 6 个块。

在检查块类型小于最大值 m_cBlocks=6 之后,它保存一个 reference 值以避免读取同一个块两次。

调用了 ReadMetadataBlock,读取 block 1 而不是 block 0 的问题将在此函数内部。

如果一切正常,它将使用 cbImage 作为大小进行分配,并将地址存储在 m_rgBlocks 的字段 block 0->pbImage 中。

!pool 命令显示分配的 tag 和 size。

所以,我已经有了存储在 m_rgBlocks 中的 block 0 的 pbImage 地址,因此我需要看看为什么它在那里复制了 block 1 的字节而不是 block 0 的字节。
我到达了对 CClfsContainer::ReadSector 的调用,其中传递了一个指向包含 pbImage 的变量的指针,以写入字节。

注意单步执行 ReadSector 时 pbimage 内容的变化。

将 0x8a 添加到 pbImage,我可以找到正确的值 4 而不是 0x13,因此问题肯定发生在之后。
调用 ClfsDecodeBlock 后,它返回一个错误 0x0C01A000A。
CClfsBaseFilePersisted::ReadMetadataBlock+153 调用 ClfsDecodeBlock
在此错误之后,它将类型加 1 并再次调用 CClfsBaseFilePersisted::ReadMetadataBlock,但使用类型 1 来读取 block 1。

在 CClfsBaseFilePersisted::ReadMetadataBlock 中,它为 block 1 分配并存储一个新的 pbImage 到 m_rgBlocks。

Blocks 0 和 1 具有不同的地址,现在如果我将 0x8a 添加到 block 1 的地址,其值为 0x13。
也许因为 block 0 返回错误,它使用了 block 1 并将其作为控制块返回给 GetControlRecord。
如前所示,当使用 0x13 值而不是 4 时,它会超出 m_rgBlocks 的范围,并读取由我控制的管道喷射值。
然后它释放 block 0 的 pbImage,并将 block 1 的指针复制到 block 0。

有必要找出导致 ClfsDecodeBlock 内部错误 0x0C01A000A 的值。
在 ClfsDecodeBlock 内部,第一个块的 checksum 为零,这就是错误 0xC01A000A。

在调用 AddLogContainer 之前,使用十六进制编辑器打开任何 spray blf 文件时,checksum 被更改为 zero。

它应该在此之前被更改,即当它使用 CreateLogFile 打开时。

由于某种原因,spray blf 文件在退出 CreateLogFile 后,block 0 的 checksum 等于 0,并返回一个 有效的句柄,让我们看看为什么会发生这种情况。
我在 CreateLogFile 打开某个 spray blf 文件之前停止。

注意,在调用 CreateLogFile 之前,spray files 的 block 0 具有正确的 checksum,并且在完成该函数后,checksum 值变为零。

因此,我在 CClfsBaseFile::GetControlRecord 上设置一个断点,以便查看内部。
通过 CClfsContainer::ReadSector 后,checksum 不为零。
在进入计算 CRC32 之前,它将内存中的 checksum 字段设置为零以计算 CRC,结果是正确的。


然后它检查 eExtendState =2 的值,并转到 WriteMetadataBlock。

这里内存中的 checksum 仍然为零,我只需要看看这个值何时被写入文件。

它检查一些在 blf spray 文件中精心构造的值,以到达 CClfsBaseFilePersisted::ExtendMetadataBlock。

在一个循环读取尚未读取的块之后,block 0 继续使用 checksum = 0。

到达 WriteMetadataBlock。

因为我是在它用 1 替换 block 0 之前运行的,blf spray 文件的 iFlushBlock 值仍然是正确的 4。

现在它正在处理 block 4,并将 block 4 写入文件,这里还不是问题所在。
然后它到达 CClfsBaseFilePersisted::FlushControlRecord

在内部,它到达 WriteMetadataBlock,但使用 参数 0,将 block 0 写入文件。

然后 ClfsEncodeBlock 返回错误 0xC01A000A,尽管它将在下方的 CClfsContainer::WriteSector 中写入带有错误 block 0 的文件。

变量 var_54 存储了 0xC01A000A 错误值,并在退出函数前进行检查。

但在调用 CClfsContainer::WriteSector(未返回错误)之后,var_54 的内容被零覆盖。
因此,该函数返回零且没有错误,继续工作,因为 CreateLogFile 将返回一个 句柄 而不是错误值。

iFlushBlock 中的值 0x13 导致它 越界,并且它将读取管道中的指针,该指针指向 trigger blf 的 Base Block +30。
然后它将0x28添加到该指针(从trigger blf基块开始的0x58),该指针的值为0x369。


INC指令会将值0x14增加1,并重复4次,因此0x14最终变为0x18。
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
之后,调用CreateLogFile,并读取0x1858值。

GetSymbol检查之前由偏移量0x1858指向的、在trigger blf中创建的假块是否具有正确的值。

如果指针没有被多次递增,它原本会是0x1458并指向正确的块。
退出GetSymbol后,它将在这里使用那个假块。

然后它将读取假块偏移量0x18处的值,我在那里放置了0x05000000,并跳转到那里的内容。
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

它读取0x05000000及其0x05001000的内容,那里就是ClfsEarlierLsn。

此函数用于在RDX中返回值0xFFFFFFFF,尽管第一次该值未被使用。
第二次调用在此处发生,它调用位于0x501000 +8的PoFxProcessorNotification。

WINDBG>dps 00000000**'05001000**
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn 00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

在此函数中RCX = 0x05000000,它检查0x40字节后必须非零。
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
跳转地址将是0x68之后。
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
参数将在0x48字节之后。
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
ClfsMgmtDeregisterManagedClient是一个方便的函数,因为我可以控制参数,而且我还有两次跳转到由我控制的函数。

第一次调用再次指向ClfsEarlierLsn,它在RDX=0xFFFFFFFF中返回。


它将从RDX=0xFFFFFFFF的内容中获取写入源。
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
在地址0xFFFFFFFF处,我存储了system_EPROCESS & 0xfffffffffffff000。

目标是位于0x5000400 +0x48的指针。
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

内核中的PipeAttribute指针指向一个填充了"A"的缓冲区,此指针将被SYSTEM EPROCESS指针的高位部分覆盖。
此指针是在我之前使用充满**"A"的缓冲区调用_NtFsControlFile**时创建的。

可以使用NtFsControlFile读取该属性的内容。

现在管道属性不再指向带有**"A"的缓冲区,而是指向system_EPROCESS & 0xffffffffffffff000**。

此代码将重复执行,直到检索到系统令牌。


在Windows 11上,系统令牌位于最近读取的EPROCESS结构的偏移量0x4b8处。

我只需要通过调用CreateLogFile将该系统令牌写入我的进程。

要完成此任务,只需重复用于读取系统令牌的步骤。

在双重调用中,它首先调用ClfsEarlierLsn以在RDX中返回0xFFFFFFFF,然后调用nt_SeSetAccessStateGenericMapping。

我检查RDX指向的值是否为系统令牌。

我的进程的令牌是:

它将写入那里。
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
现在我的进程是System,我可以运行记事本来验证。



BINDIFF显示了许多更改过的函数

易受攻击的函数在这里:


主窗口是打了补丁的版本,次窗口是易受攻击的版本。
补丁测试CflsEncodeBlock的返回值(即0xC01A000A),将其存储到变量var_54中,由于该值为负,检查后避免执行WriteSector。
补丁除了不写入文件外,函数正确返回0xc01a000a,因此CreateLogFile不返回任何句柄,漏洞利用无法继续。


仅当ClfsDecodeBlock不为负时,它才进入WriteSector,但最终仍返回负值0xC01A000A。
这就是实际阻止使用我刚附上的PoC进行漏洞利用的补丁。
至此,我们已解释了如何利用该漏洞,它导致我们可以控制函数,从而读取SYSTEM令牌并将其写入我们自己的进程,以实现本地权限提升。你可以在Fortra的GitHub上找到功能性PoC。
我们希望你觉得它有用,如有任何疑问,请联系我们: