这篇文章的目的是介绍今年出现的两个CLFS漏洞汇总分析.
文章结合了逆向代码和调试结果分析了CVE-2021-24521和CVE-2022-37969漏洞利用过程和漏洞成因.
CVE-2021-24521漏洞的成因是 SignatureOffset 字段的范围校验不严格,从而导致SignatureOffset 指向的区域可以与某个 ContainerContext 的 pContainer 指针重合.攻击者可以通过精心构造日志文件内容来触发这个漏洞,CLFS在解析日志文件时,会调用 ClfsEncodeBlock 函数将每个 512 字节扇区的最后两字节备份到 SignatureOffset 指向的偏移处,整个“备份”完成后,pContainer 指针会被替换为攻击者伪造的指针,具体相关分析清读者移步相关引用中的分析文章这里不再赘述。
首先借助漏洞,将内存中的 pContainer 指针覆盖为 fakeContainer 指针,并且事先已经伪造了 fakeContainer 的虚函数表。通过利用漏洞内存中的 ContainerContext 对象和其内部的 fakeContainer 指针,我们可以看到这个指针变为了一个用户态地址,它指向的虚表也变为了一个用户态地址。正常情况下,这两个地址应该位于内核空间。
接下来我们看一下在野样本是如何实现任意地址写入的,在 RemoveContainer 函数内,代码会尝试获取 pContainer 指针,并调用内部的两个虚函数。正常情况下这两个函数是 Release 和 Remove,把地址虚函数的地址替换为没有任何实际操作的ClfsSetEndOfLog函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
关闭blf文件的调用这2个函数之前还会调用一次CClfsContainer::Close,由于pContainer是我们精确控制的内核态地址,把field_DeviceObjectHint_20置为一个任意的打开文件句柄,field_Device_Object_30置为要被ObfDereferenceObject递减Thread的PreviousMode地址, 将当前线程模式改为内核模式,这是一种直到windows11微软允许的利用方式,详见微软文档,这样就可以允许用户态访问内核态内存通过NtWriteVirtualMemory将当前进程自身的令牌替换为 System 进程的令牌,从而完成提权。
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 |
|
在四月的CVE-2021-24521和九月的CVE-2021-24521,存在一个不知名的clfs漏洞,具体方式是通过一个混肴的CLIENT_CONTEXT和CONTAINER_CONTEXT结构公用同一片符号表内存,用实际只相差8大小的地址空间,导致CLFSHASHSYM结构的下个cbSymName和cbOffset正好是上个不做验证ulBelow和ulAbove字段,在关闭CONTAINER时绕过AcquireContainerContext检查还原重叠的PCLFS_CONTAINER_CONTEXT的pContainer 指针,在CONTAINER_CONTEXT结构后是原来的Container路径字符串,由于重叠的关系CLIENT_CONTEXT后的路径字符串没有对应的正确内容,但是clfs没有去验证这个限制,所以就实现了绕过类型混肴,具体利用方法片段如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
利用这个伪造的pContainer 指针,只要关闭blf文件句柄就会在析构函数中调用CClfsLogFcbPhysical::Finalize中文件中将原始 pContainer 指针从PCLFS_CONTAINER_CONTEXT读出,判断cActiveContainers字段是否大小大于0,当容器队列cidQueue值为-1时,最后调用pContainer自动关闭函数和指向其虚表的析构函数.可以采用CVE-2021-24521同样的利用方式利用.这2个漏洞的差别在于CVE-2021-24521的补丁修复了CClfsBaseFile::ValidateRgOffsets了SignaturesOffset,只是修复了Signature是否与符号相交的情况,却没修复符号表内部存在重叠混肴的情况.
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
|
实际上clfs确实在CClfsBaseFilePersisted::LoadContainerQ中将CONTAINER_CONTEXT中pContainer指针覆盖为新申请的容器对象实例指针,但是由于符号表相交的关系在之后FlushMetadata中获取的CLIENT_CONTEXT是与CONTAINER_CONTEXT存在重叠的内存.存在漏洞的伪代码如下cltctx->llCreateTime.QuadPart = that->field_CtrateTime_1a0;这行代码将ClientContext+20的位置也就是CONTAINER_CONTEXT+18的值替换回之前CClfsLogFcbPhysical::Initialize初始化时保存在CClfsLogFcbPhysical->field_CtrateTime_1a0.上下文的旧值.这里需要绕过的是判断当前的日志是不是Multiplexed类型,所以采用多数据流Log:<LogName>[::<LogStreamName>]创建日志文件对象就可以绕过这个限制,具体详见微软CreateLogFile文档
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 57 58 |
|
FlushImage又调用WriteMetadataBlock在该函数中,首先会遍历每一个容器上下文,将pContainer先保存后置为0:然后调用ClfsEncodeBlock函数,对数据进行编码,此时记录中每0x200字节的后两个字节将被写入到SignaturesOffset指向的内存中,接着调用CClfsContainer::WriteSector函数,然后调用ClfsDecodeBlock函数对数据进行解码,并将之前保存的pContainer值重新写回.我们看下调试结果
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
|
CVE-2021-24521漏洞的成因是由于缺乏对 CLFS.sys 中基本日志文件 (BLF) 的基本记录头中的字段 cbSymbolZone 的严格边界检查。 cbSymbolZone存在一个名为CLFS_BASE_RECORD_HEADER的结构体,在此复制一下,本文中所有用到的结构体均可在blf.bt(010编辑器分析blf文件)附件中找到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
可以看到其中就包括了一个名为cbSymbolZone的成员。在IDA中搜索该成员名字,可以找到唯一个操作该成员的函数CClfsBaseFilePersisted::AllocSymbol:
如果字段 cbSymbolZone 设置为无效偏移量,则会在无效偏移量处发生越界写入.
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 |
|
可以看出,在这里cbSymbolZone成员的作用是标识在BLF文件中,CLFS_BASE_RECORD_HEADER后所有符号表的路径字符串(符号)总共占用了多少空间.在内存中,当新增添加一个Container时,使用该成员用来计算新增的Container应该跳过多少空间往后排(即跳过现有的最后一个container的路径字符串),并申请sizeof(CLFSHASHSYM)也就是0x30大小空间,然后清零这片内存.但该函数直接使用使用了来自BLF文件中存储的cbSymbolZone的值,该值可以是大于0、小于到结尾的任意值。举例来说就是,它可以让跳过的空间很少,从而改写了现有最后一个container路径字符串,更小则可清零container结构本身的pcontainer指针。pcontainer是内核态指针位于ffff000000000000000区域,如果高20位被清零,那截断后的地址低12指针地址就可以指向用户态可申请内存地址,VirtualAlloc申请0x10000000大小内存后就可以完全覆盖这片地址.由于pcontainer指针总是一0x10字节对其的,可以采用替换虚表函数为SeSetAccessStateGenericMapping方法利用.
利用的方式也是一样修改Thread的PreviousMode字段,这个函数作用就是对rcx+8的地址自增实现相同的效果,所以只要把所有的每0x10个字节把+0处填为虚表,+8处填为PreviousMode地址就可以了.在这里有一个需要绕过的地方就是CClfsContainer::Close处.
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 |
|
这里pcontainer指针总是一0x10字节对齐的,所以我们无法预估pcontainer+20和+30的检测点字段可能会出现在什么位置,走到CClfsContainer::Close里面和之前的一样函数利用的话就会因为要关闭的句柄不是合法的出错,一种可行的方案是DeleteLogByHandle或者原文的NtSetInformationFile->FileDispositionInformation方式将file->clientshuwdown_15c设为0x10这样就会走到上方伪代码的第一个if入口在里面直接调用container的虚表函数,从而绕过了CClfsContainer::Close检查.这里有个限制DeleteLogByHandle会对CreateLogFile的参数进行检查只要赋予所有读写权限GENERIC_ALL即可绕过这个检查.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
CVE-2022-37969分析原文的利用方式比较麻烦,我先来阐述下具体利用步骤.
1.创建主blf文件通过CreateLogFile并关闭文件
2.创建一大堆的blf文件,通过查询BigPoolInformation确保最后申请的2个blf文件的MetadataBlock相隔距离正好为0x11000字节,并关闭最后申请的2个blf文件
3.篡改主blf文件MetaBlockScratch内容为以下代码,包括构造FAKE_CLIENT_CONTEXT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
4.重新打开修改后主blf文件通过CreateLogFile
5.创建副blf文件通过CreateLogFile
6.对副blf文件调用DeleteLogByHandle或者原文的NtSetInformationFile->FileDispositionInformation切换析构模式
7.对副blf文件添加容器通过AddContainer
8.对主blf文件添加容器通过AddContainer
9.关闭所有句柄触发漏洞
CLFS的元数据块总数默认为6个也就是如下的元数据类型,对于不是这个常量的元数据数量可以产生漏洞CVE-2022-3022详见看雪分析,在ReadImage先读取第一个ClfsMetaBlockControl元数据块,该块默认大小为400不能任意更改,读取后获取控制块中的所有元数据块配置数组,根据元数据块配置CLFS_METADATA_BLOCK读取和它之后的影子块保存在pbImage字段中,这个字段仅内核模式可见不写入文件中
1 2 3 4 5 6 7 8 9 |
|
数据块类型 | 元数据块类型 | 描述 |
---|---|---|
Control Record | Control Metadata Block | 包含了有关布局(layout)、扩展(extend)区域以及截断(truncate)区域的信息 |
Base Record | General Metadata Block | 包含了符号表信息,其中包括该BLF有关的客户端、容器和安全上下文信息 |
Truncate Record | Scratch Metadata Block | 包含了因为截断操作而需要对扇区进行更改的客户端信息,以及具体更改的扇区字节. |
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 |
|
对于打开和创建的blf文件,在之后内存池空间没有被占位的情况下,ClfsMetaBlockGeneral和下个blf文件的ClfsMetaBlockGeneral几个间隔块大小经调试得出的结构偏移是常量0x11000,微软为我们提供个一个用户态函数可以查询到SystemBigPoolInformation具体的堆地址和tag,代码如下.这个查询函数可以查询到完整的大块堆信息,只要在申请之前查询一次申请之后查询一次即可准确分析出当前元数据所在的内核堆地址.当申请占位的文件最后堆块两个间隔正好是0x11000时,关闭这个两个占位文件,那些接下来申请的两个主文件中的ClfsMetaBlockGeneral也会出现在这个位置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
也就是说将cbSymbolZone设置为偏移0x11000加上pcontainer指针在元素据块中的偏移就能清空下个blf文件pcontainer指针的高位部分,实现和上个漏洞相同的利用结果.但是这里有个限制需要绕过,也就是cbSymbolZone需要小于SignaturesOffset,为了绕过这个限制,原文采用了一个巧妙的方法也就是构造一个特定的CLIENT_CONTEXT,在打开blf文件时如果在CClfsLogFcbPhysical::Initialize判断eState = CLFS_LOG_SHUTDOWN就会进入就会调用ClfsLogFcbPhysical::ResetLog
1 2 3 4 5 6 7 8 |
|
CLIENT_CONTEXT这里lsnRestart_58所在元数据位置正好是第4个Signatures要写入的位置,被替换成了CLFS_LSN_INVALID也就是FFFFFFFF00000000重叠部分正好是FFFFFFFF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
CVE-2021-24521只是修复了Signatures与符号表相交的情况,并没有限制SignaturesOffset可以与自身的偏移量0x68重叠的情况,这里将SignaturesOffset设置为0x50那么第4个Signatures所在位置0x19FE(0xC*0x200+0x1FE)就会写入SignaturesOffset在文件中的偏移量68加上2的高16位也就是FFFF刚才提到的重叠部分的数据,这个利用方式很巧妙笔者还未发现被微软完全修复.SignaturesOffset被替换后的值变成了0xFFFF0050,从而绕过了cbSymbolZone限制使其可以越界清空下个堆块数据.
但是这种利用方式比较复杂,需要精确控制堆申请的偏移量,实际环境利用难度过高,其实还有一个更简单的利用方式,这种方式不需要清空下个堆块的pcontainer指针而是清空自己文件的指针,同样可以实现利用,实现方法只需要设置cbSymbolZone等于本文件的pcontainer指针偏移量+3就可以了,最后利用的效果是一样的.
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
|
CVE-2021-24521补丁添加了Container和ClientContext对cbSymbolZone的越界验证,从而修复了所有的符号表相交和覆写问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
出于安全原因笔者不能提供完整的poc代码,下图是笔者在打了8月补丁的Windows1021h2虚拟机上成功复现了CVE-2022-37969
作者来自ZheJiang Guoli Security Technology,邮箱[email protected]