红雨滴团队的《CVE-2023-28252在野提权漏洞样本分析》文章详细的分析了恶意文件并定位到漏洞触发点,但由于文章的主要目的是分析恶意文件而非讲解漏洞成因,因此漏洞成因方面的内容偏少。该文的主要内容是分析clfs.sys内的函数,以了解漏洞触发过程。由于笔者刚入门,如果文章出现错误,请多多包涵。
通用日志文件系统(Common Log File System,缩写CLFS)是一个通用目的的日志文件系统,它可以从内核模式或用户模式的应用程序访问,用以构建一个高性能的事务日志。
文件格式的类型定义可以参考ionescu007的文章,下面进行简单介绍。
元数据块类型为6种,其中Shadow块为备份块,当主块存在异常时,会使用Shadow块。类型定义如下:
1 2 3 4 5 6 7 8 9 |
|
每个元数据块都以CLFS_LOG_BLOCK_HEADER结构起始,后面跟着元数据块特有的结构与数据,简单示意图如下:
该漏洞涉及ClfsMetaBlockControl和ClfsMetaBlockGeneral块,其涉及的主要类型定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
其中ClfsMetaBlockGeneral块包含容器类型,且容器的pContainer成员为内核对象指针,因此如果可以控制该成员,则将为执行任意代码打下基础。其结构定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
之前在CVE-2022-24521漏洞复现过程中,简单编写了一个010 Editor的模板,可以简单解析日志文件(.blf格式文件),便于了解文件结构布局,后续可在附件中下载。
漏洞通过 CreateLogFile和AddLogContainer 函数进行触发,接下来将根据构造的日志文件内容梳理这两个函数的执行流程。为了便于理解clfs.sys的函数,首先将ionescu007文章中的CLFS相关结构体整理成了头文件,并加载到IDA中。
《CVE-2022-37969 技术分析》中说到当在用户空间调用CreateLogFile函数时,CLFS!CClfsRequest::Create负责处理这个请求。当使用 CreateLogFile 函数打开现有基本日志文件时,将在 CLFS.sys 中调用函数CClfsLogFcbPhysical::Initialize ,并随后调用CClfsBaseFilePersisted::OpenImage。接下来将从OpenImage函数开始进行分析。
OpenImage首先进行初始化操作,如申请内存。
图:OpenImage部分代码
OpenImage中调用了CClfsBaseFilePersisted::ReadImage函数,这里提前说明下函数做了哪些后续要关注的工作。26行将this+0x28赋值为6,27行为this+0x30 申请内存空间,45行调用CClfsBaseFile::GetControlRecord获取控制记录的地址。控制记录为_CLFS_CONTROL_RECORD结构,为ClfsMetaBlockControl块的第二个结构体。
图:ReadImage部分代码
随后OpenImage函数的131行调用CClfsBaseFilePersisted::ReadImage函数加载日志文件,获取控制记录结构地址,加载成功后从142行代码开始执行。146行调用CClfsBaseFile::GetBaseLogRecord函数获取基本日志记录地址,该返回值为_CLFS_BASE_RECORD_HEADER指针,指向ClfsMetaBlockGeneral块的第二个结构体,随后在149-157行使用基本日志记录初始化数据。当控制记录的eExtendState不为0,iExtendBlock小于4,iFlushBlock小于6且大于等于iExtendBlock,cExtendStartSectors小于文件扇区总数量,cNewBlockSectors小于扩展元数据块的扇区总数量加上(iExtendBlock/2)时,将在179行调用CClfsBaseFilePersisted::ExtendMetadataBlock函数。
图:OpenImage部分代码
ExtendMetadataBlock函数98行根据参数2(iExtendBlock)加载元数据块,然后获取控制记录指针。当控制记录的eExtendState成员值为2时,跳转到LABEL_41。
图:ExtendMetadataBlock部分代码
192行检测eExtendState值是否为2,不为2则退出循环。随后在208行使用iFlushBlock作为参数2调用CClfsBaseFilePersisted::WriteMetadataBlock函数(本次调用该函数时,参数2被指定为了4,用于扩展ClfsMetaBlockScratch块,这一过程只是为了过渡到FlushControlRecord函数的调用)。随后209行检测iFlushBlock与iExtendBlock值是否相同,相同则v12复制为0,从而在下次for循环时,从192行的检测中退出循环。循环最后调用了CClfsBaseFilePersisted::FlushControlRecord,这个函数需要重点关注。
图:ExtendMetadataBlock部分代码
FlushControlRecord函数在处理部分数据后,于23行调用了CClfsBaseFilePersisted::WriteMetadataBlock。
图:FlushControlRecord代码
WriteMetadataBlock函数在31行根据参数2获取指定元数据块的地址,然后进行了部分数据的更新,此时更新的元数据块是iFlushBlock字段指定的。61行调用了ClfsEncodeBlock处理块数据,并在67行调用CClfsContainer::WriteSector将写入块数据,这两个函数是漏洞利用的关键前置步骤。需要注意的是,ClfsEncodeBlock和WriteSector操作的是参数2指定的元数据块,本次参数2为0,即操作的是ClfsMetaBlockControl块。
图:WriteMetadataBlock部分代码
ClfsEncodeBlock先将_CLFS_LOG_BLOCK_HEADER的Checksum 成员值置为0,然后调用ClfsEncodeBlockPrivate函数进行下一步处理。当ClfsEncodeBlockPrivate返回结果为负数时,则不会重新计算校验和并返回。
图:ClfsEncodeBlock代码
ClfsEncodeBlockPrivate检测_CLFS_LOG_BLOCK_HEADER的ValidSectorCount是否小于TotalSectorCount,如果小于则返回错误码0xC01A000A。
图:ClfsEncodeBlockPrivate部分代码
回顾WriteMetadataBlock函数的61行代码,可以发现并没有对ClfsEncodeBlock的返回值进行判断,而是直接调用WriteSector函数将更新数据。假设ValidSectorCount小于TotalSectorCount,执行ClfsEncodeBlock时则首先将校验和置为0,然后ClfsEncodeBlockPrivate检测到异常,返回错误码0xC01A000A,因此ClfsEncodeBlock不会更新校验和。随后直接调用WriteSector函数将指定元数据块的校验和的值更新为了0。
图:WriteMetadataBlock部分代码
将校验和值更新为0的目的在于绕过OpenImage中的检测。OpenImage的163~173行,检测了iFlushBlock和iExtendBlock的值是否大于6,当大于6时则会返回错误码0xC01A000D。简介文件格式时,提到过shadow块为备份块,当主块的校验和值为0时,则会检测失败,从而使用shadow块的数据,因此shadow块数据可以为大于6的数据,然后通过构造特定字段值,执行上述流程使校验和为0,下次获取元数据块则使用shadow块,下一部分内容会说明替换逻辑。
图:OpenImage部分代码
《样本分析》中指出WIN32API AddLogContainer对应内核中的CLFS!CClfsLogFcbPhysical::AllocContainer函数,下面将从AllocContainer函数入手分析。
AllocContainer函数的69行调用了CClfsBaseFilePersisted::AddContainer函数。
图:AllocContainer部分代码
AllocContainer函数的44行调用了CClfsBaseFilePersisted::AddSymbol函数。
图:AddContainer部分代码
AddSymbol函数中当v16 值为 0xC0000023时,会在26行调用CClfsBaseFilePersisted::ExtendMetadataBlock函数。v16为CClfsBaseFile::FindSymbol函数的返回值,因此关注FindSymbol函数。
图:AddSymbol代码
FindSymbol函数中没有发现0xC0000023错误码,但124行调用了虚函数,并且返回值为负数时将通过LABEL_26标记返回。使用windbg设置断点调试,最终确定该虚函数为CLFS!CClfsBaseFilePersisted::AllocSymbol()。
图:FindSymbol部分代码
在AllocSymbol中的25行发现了错误码0xC0000023,其触发条件为当前容器的结束地址大于签名缓冲区地址。当前容器的结束地址根据_CLFS_BASE_RECORD_HEADER结构的cbSymbolZone成员值计算得到,因此可以修改cbSymbolZone值使其满足条件返回错误码0xC0000023,从而在AddSymbol中调用ExtendMetadataBlock函数。
图:AllocSymbol代码
此时执行流程为:
AllocContainer -> AddContainer -> AddSymbol -> FindSymbol -> AllocSymbol //返回错误码0xC0000023
AllocContainer -> AddContainer -> AddSymbol -> ExtendMetadataBlock //根据错误码0xC0000023,调用ExtendMetadataBlock函数
ExtendMetadataBlock函数105行调用CClfsBaseFile::GetControlRecord获取控制记录地址(_CLFS_CONTROL_RECORD),控制记录存在于ClfsMetaBlockControl块中。在CreateLogFile执行过程中,通过构造特定的字段使的CreateLogFile校验和为0,因此本次获取的控制记录实则为ClfsMetaBlockControlShadow块的。下面分析GetControlRecord函数,查看控制记录是如何被替换的。
图:ExtendMetadataBlock部分代码
GetControlRecord函数18行调用CClfsBaseFile::AcquireMetadataBlock函数加载ClfsMetaBlockControl块,随后进行一系列检查,最终将ClfsMetaBlockControl中CLFS_CONTROL_RECORD的地址写入到参数2。
图:GetControlRecord代码
AcquireMetadataBlock中调用了一个虚函数,在windbg中跟踪为CClfsBaseFilePersisted::ReadMetadataBlock函数。
图:AcquireMetadataBlock代码
ReadMetadataBlock函数31至52行代码读取参数2指定的元数据块到分配的池内存,随后在58行调用ClfsDecodeBlock函数进行解码(因为CreateLogFile函数处理流程中ClfsMetaBlockControl校验置为了0,因此该函数会返回错误码)。73行代码再次调用ReadMetadataBlock函数,加载当前元数据块的shadow块。随后通过一系列检查到达75行,此时v8小于0,将跳转到117行代码开始执行。117~122代码,释放了ReadMetadataBlock为当前元数据块申请的池空间,然后使用其shadow块地址代替当前元数据块。
图:ReadMetadataBlock部分代码
图:ReadMetadataBlock部分代码
ClfsDecodeBlock函数先判断校验和是否为0,这里为0然后跳转到16行继续判断。通过ReadMetadataBlock的函数调用可知a4值为0x10,然后MajorVersion值为0xF,因此该检测失败,直接返回错误代码 0xC01A000A。至此ReadMetadataBlock函数使用ClfsMetaBlockControlShadow块替换了ClfsMetaBlockControl块。
图:ClfsDecodeBlock代码
图:默认MajorVersion值
回到ExtendMetadataBlock函数,可以得知105行代码获取的实际为ClfsMetaBlockControlShadow块。随后通过检测,跳转到LABEL_41处。
图:ExtendMetadataBlock部分代码
跳转到LABEL_41后,在208行调用了CClfsBaseFilePersisted::WriteMetadataBlock函数。需要注意的是ClfsMetaBlockControlShadow块中的iFlushBlock值为0x13,即WriteMetadataBlock的参数2值为0x13。
图:ExtendMetadataBlock部分代码
此时查看WriteMetadataBlock处理逻辑。31行代码使用24乘以参数2作为this+0x30的偏移,随后取得数据。在ReadImage中可以得知this+0x30的内存空间为0x90,而24*0x13结果为0x1C8,已经超出了0x90,从而取得了错误的地址。然后从错误地址的0x28偏移取得偏移B,然后将错误地址的偏移B的数据进行+1。此时通过漏洞已经触发,实现了数据自增1。
图:WriteMetadataBlock部分代码
《样本分析》中指出CreateLogFile函数会调用CClfsBaseFilePersisted::CheckSecureAccess函数。CheckSecureAccess函数的55行起会遍历容器,然后64行获取pContainer值,并在69行和78行调用二次虚函数。因此可以创建一个日志文件,其包含了精心构造的容器,然后利用该漏洞修改容器的偏移,使其再次调用CreateLogFile时,遍历的容器为构造的容器,然后pContainer值指向用户空间中构造虚函数布局,这样调用虚函数时便可以执行我们想要执行的函数。由此构造函数利用链(函数利用链可以查看《CVE-2022-37969 技术分析》,该文章详细的介绍了相关内容),实现权限提升。
图:CheckSecureAccess部分代码
权限提升过程涉及到池空间分配、利用管道属性实现任意地址写入等操作,这里仅简单介绍下思路,不在详细展开,具体过程可以查看《样本分析》。
1.创建一个包含容器的日志文件A,然后容器地址偏移0x100处构造一个pContainer成员有值的容器。
2.通过NtQuerySystemInformation查询0x7A00大小的CLFS数据池。
3.调用CreateLogFile函数将日志文件A加载到内核。
4.通过NtQuerySystemInformation查询0x7A00大小的CLFS数据池,查询到新增的池地址,这个地址为日志文件A的ClfsMetaBlockGeneral块地址(该块的大小为0x7A00)。
5.创建多个漏洞利用日志文件B,构造文件数据,使其最终可以在WriteMetadataBlock中实现越界数据访问,并进行数据自增。
6.构造池布局空间。申请大量的匿名管道,然后将13个日志文件A的ClfsMetaBlockGeneral块地址写入到管道,使其申请0x90的池空间。
7.关闭匿名管道,释放池空间,然后调用CreateLogFile函数将多个日志文件B加载到内核,然后在将13个日志文件A的ClfsMetaBlockGeneral块地址写入到管道,这样在日志文件B的clsfhelp(this+0x30)的周围就是构造的日志文件A的ClfsMetaBlockGeneral块地址。
8.构造函数执行内存布局。
9.调用AddLogContainer函数触发漏洞,修改日志文件A的ClfsMetaBlockGeneral块中rgContainer数据,使其容器偏移增加0x100。增加0x100的原因是利用漏洞增加的是后24位的数据,因此自增1,等于增加了0x100偏移。
10.调用CreateLogFile函数再次加载日志文件A,随后通过CheckSecureAccess函数利用构造的容器,然后根据函数执行内存布局利用管道属性,实现任意数据写入,然后替换了进程的TOKEN从而实现提权。
根据上面分析可以得知触发漏洞是由于WriteMetadataBlock的ClfsEncodeBlock函数将校验和置为了0,然后在触发错误时,仍更新了ClfsMetaBlockControl块数据,从而导致下一次加载ClfsMetaBlockControl块时使用了导致越界访问的ClfsMetaBlockControlShadow块。因此修复后的WriteMetadataBlock函数在82行对ClfsEncodeBlock的返回值进行了检测,在出现异常时不调用WriteSector函数,从而使得ClfsMetaBlockControlShadow不会被使用。《样本分析》中描述了其他的修复内容,这里就不再重复陈述。
图:修复后的WriteMetadataBlock部分代码
https://mp.weixin.qq.com/s/Qlst6CX_z1A698Tvvx-bIQ (CVE-2023-28252在野提权漏洞样本分析)
https://github.com/ionescu007/clfs-docs/ (CLFS格式)
https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part (CVE-2022-37969 技术分析 上)
https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part2-exploit-analysis(CVE-2022-37969 技术分析 下)
最后于 14小时前 被i未若编辑 ,原因: