综述

卡巴斯基披露[1]该在野0day提权漏洞是一个越界写入(增量)漏洞,当目标系统试图扩展元数据块时被利用来获取system权限———Windows中最高的用户权限级别。该漏洞允许改变基础日志文件,作为回报,迫使系统将基础日志文件中的假元素视为真实元素。其通过改变指向内存中一个特定的公共日志文件系统(CLFS)结构的偏移值,使之指向一个恶意结构。此外其在用户层面提供一个指向受控内存的指针,以获得内核的读/写权限。CLFS结构是Windows操作系统使用的CLFS通用日志系统的一部分,它由物理日志文件、日志流、日志记录等组成。

该在野0day提权漏洞已被Nokoyawa 勒索团伙使用,以用于部署勒索软件前获取目标系统的system权限。

Microsoft 在四月补丁日修复该漏洞[2],并将其标记为CVE-2023-28252(Windows 通用日志文件系统驱动程序特权提升漏洞)。下图是在打补丁前系统上的运行截图,通过漏洞利用完成提权。

漏洞样本分析

该样本本身通过themida进行了保护,因此需要调试时过掉一开始的反调试,之后就和正常的样本分析差不多了,通过对exp样本的分析发现,该漏洞在利用及代码实现上和去年CVE-2022-37969非常相似。如下图所示,样本运行前首先清空对应的工作目录,之后调用fun_osVersioncheck/fun_osVersioncheck获取系统版本,并通过fun_osVersioncheck获取对应读取系统及当前进程的内核偏移,并初始化一系列内存。

这里fun_osVersioncheck/fun_osVersioncheck的实现和CVE-2022-37969基本保持一致,甚至初始化的关键数据结构也没有太大的变动,如下图所示,该图出自zscaler的安全研究员针对CVE-2022-37969的分析[3]

通过动态地址获取的方式分别从clfs.sys/ntoskrnl.exe中获取函数ClfsEarlierLsn,ClfsMgmtDeregisterManagedClient,RtlClearBit/ PoFxProcessorNotification,SeSetAccessStateGenericMapping,其中ClfsMgmtDeregisterManagedClient及PoFxProcessorNotification这两个工具函数在CVE-2022-37969中被没有使用。

在0x5000000位置分配0x1000000长度的内存,注意0x5000000这个地址的使用也和CVE-2022-37969一致。

接下来获取NtFsControlFile函数地址,并通过ZwQuerySystimeInformation获取PipeAttributer的内核对象地址,在0xFFFFFFFF上分配长度为4096的内存,并以此部署system Process token,熟悉CVCE-2022-37969利用的话就知道这个位置使用于辅助ClfsEarlierLsn/SeSetAccessStateGenericMapping进行最终的内存写入。

进入该exp 的核心部分,函数fun_prepare中通过CreateLogFile创建第一个log file,这里称之为trigger clfs,之后循环调用fun_trigger再次创建10个log file,这里称之为spray clfs[i]

细看fun_prepare/fun_trigger这两个函数中的log file是如何构造的,首先是fun_prepare,核心部分代码如下所示:

可以看到其主要是修改了CLFS log Block Header Record offsets Array[12]的位置,此外依次在base block及base block shadow的other data中修改了16个字节的数据,这里注意base block及base block shadow一致。

之后通过写入clfs文件,并修复对应的crc校验值,最后调用AddLogContainer增加一个log container,需要注意对应的trigger clfs base block 内核地址para_clfsKerneladdress通过ZwQuerySystemInforation搜索的方式获取,其原理是通过搜索0x7a00大小标志位clfs的pool,类似包括pipeAttribute的内核地址也是通过该方式获取。

Spray clfs[i]中修改的位置就比较分散了

这里注意Spray clfs[i]生成之后,在这个位置并没有调用AddLogContainer

Spray clfs[i]中响应的结构如下所示,重点需要注意的位置是control block及control blok shadow两个对应的位置做了修改,control blok shadow中被修改为了0x13,此外base block中的cbsyblozone被设置为0x65c8,其对应的base block位置保持一致。

之后,代码进行了一系列内存spray的操作。首先trigger clfs 对应的内核base block内核地址+0x30的位置被循环赋值到一个数组v93中,然后两次调用函数fun_pipeSpray,对应的参数分别为0x5000及0x4000。

fun_pipeSpray为一个pipe的spray,其根据参数传入的数量生成指定数量对数的pipe(read/write),第一次fun_pipeSpray调用传入0x5000,因此生成了0x5000对pipe(read/write),这里统一将这0x5000对pipe称之为pipeA,第二次的0x4000对称之为pipeB。

遍历0x5000对pipA,并调用其writepipe写入包含trigger clfs base block + 0x30的数组v93,遍历结束,从pipeA(0x2000偏移),第174对pipe开始释放,一共释放0x667对pipe对。

释放结束后,紧接着通过前面的spray[i] clfs循环调用CreateLogFiles,这里大概率就是一处内存占位,用CreateLogFiles调用中某一处内存对象占据前面pipA中释放的pipe对。

CreateLogFiles循环占位结束后,遍历0x4000对pipB,并调用其writepipe写入前面数组v93。

这一系列操作结束后的内存结构如下

start of pipA(trigger clfs + 0x30) ... spray clfs[i] ....pipB(trigger clfs + 0x30)..end of pipA(trigger clfs + 0x30)

完成内存spray之后,遍历spray clfs[i],为每一个 spray clfs调用AddLogContainer以增加一个log container,之后布局0x5000000中的内存空间。

完成0x5000000的内存布局后,调用CreateLogFile,此时调用的clfs对象是trigger clfs,CreateLogFile调用完成即可通过fun_NtFsControlFile读取system process的token,从这里就可以看到CreateLogFile调用之后应该就触发了漏洞,完成了和之前CVE-2022-36979一样的操作,即执行了内存0x50000000中的内容,完成了对PipeAttribute内核对象的修改,从而使得fun_NtFsControlFile能实现任意地址读取。

之后重复调用CreateLogFile触发漏洞,完成进程token的替换。

此外样本中同样也支持修改priviousMod,实现任意地址读写来提权的方式。

通过分析以上的利用代码可以发现,该漏洞在利用上和之前的CVE-2022-36979有很多类似的地方,关键在于通过漏洞疑似修改了container pointer,在该漏洞中container pointer疑似被指向0x5000000,攻击通过布局0x5000000,依赖以下工具函数实现任意地址写入,这里同样和CVE-2022-36979类似,但是,该工具链中增加了函数:

  • PoFxProcessorNotification

  • ClfsMgmtDeregisterManagedClient

最终的调用链为:

  • PoFxProcessorNotification

  • ClfsMgmtDeregisterManagedClient

  • ClfsEarlierLsn

  • SeSetAccessStateGenericMapping

该漏洞利用和CVE-2022-36979的不同之处在于,CVE-2022-36979中漏洞本身的触发很简单,但在触发前进行更为复杂的操作,这里我们将其触发前的代码操作进行一下总结。

1. Fun_prepare中生成一个trigger clfs,其中对应的位置被设定为0x5000000,并调用AddLogContainer。

2. CreateLogFile创建10个spray clfs[i]

3. trigger clfs的base block address+0x30被pipe spray,具体如下:

3-1.0x5000对 pipeA(read/write)

3-2.0x4000对 pipeB(read/write)

3-3.pipeA写入包含12个trigger clfs base block address+0x30地址的数组

3-4.pipeA(0x2000偏移),第174对pipe开始释放,一共释放0x667对

3-5.10个spray clfs再次调用CreateLogFile,这里应该是为了占位前一步中释放的0x667对pipe

3-6.遍历pipeB写入包含12个trigger clfs base block address+0x30地址的数组

spray完毕后大致的内存如下:

pipA

0x2000

...

spray clfs[n] size 0f 7a00 + 0xDB对pipB

...

0xACDA(0x2000 + 0x667 * 16)

end

4. 遍历针对第n个spray clfs[i]调用AddLogContainer

5. 针对trigger clfs调用CreateLogFile

结合上述的流程,这里猜测第四步中第n个spray clfs[i]调用AddLogContainer将会导致下述内存结构中spray clfs[i]通过相邻的pipB(trigger clfs + 0x30)对trigger clfs base block内存进行破坏,从而导致之后第五步trigger clfs调用CreateLogFile时调用了错误的container pointer,该pointer指向0x500000,最终进入攻击者控制的内存中,并通过一系列辅助函数链最终达成任意地址写。

start of pipA(trigger clfs + 0x30) ... spray clfs[i] ....pipB(trigger clfs + 0x30)..end of pipA(trigger clfs + 0x30)

漏洞原理分析

第一步需要确认我们的猜测是否正确,即是否是调用了错误的container pointer,该pointer指向0x500000,这里最简单的方法就是针对CLFS!ClfsEarlierLsn下断点,因为该函数是0x500000这段内存函数调用链的开始,通过它可以找到漏洞触发时是如何进入到该地址执行的。

针对函数CLFS!ClfsEarlierLsn下断点,CreatelogFile函数调用完毕之后,内核中触发进入了CLFS!ClfsEarlierLsn调用。

往上回溯,CLFS!ClfsEarlierLsn是通过0x500000这个位置进入,且这里从代码上看,大概率是破坏了对应的container pointer。

进入CLFS!ClfsEarlierLsn调用。

触发0x500000内存代码执行的函数为CLFS!CClfsBaseFilePersisted::CheckSecureAccess,可以看到恶意的container pointer来自v29,而v29来自于函数CLFS!CClfsBaseFile::GetSymbol。

CLFS!CClfsBaseFile::GetSymbol中v29的值来自于v17,v17由BaseLogRecord + v6共同决定,这里BaseLogRecord是一个固定的值,因此需要看看v6来自于哪里,通过代码可知,v6的值为函数CLFS!CClfsBaseFile::GetSymbol的第二个参数传入。

因此返回CLFS!CClfsBaseFilePersisted::CheckSecureAccess,可以看到CLFS!CClfsBaseFile::GetSymbol的第二个参数为poi(BaseLogRecord + 0xCA)。

这里在CLFS!CClfsBaseFilePersisted::CheckSecureAccess下断,可以看到传入CLFS!CClfsBaseFile::GetSymbol前poi(BaseLogRecord + 0xCA)的值是0x1570。

作为CLFS!CClfsBaseFile::GetSymbol的第二个参数传入。

计算返回对应的v29,如下所示,返回指针的0x18位置就指向0x5000000,细心的读者可以发现该指针指向的位置其实就是trigger clfs中other data域中构造的内容。

之后代码会依次检测该指针附近的几个值是否符合规定,这些检测的字段也都是trigger clfs一开始构造的部分。

对应的检测代码如下所示

之后返回CLFS!CClfsBaseFilePersisted::CheckSecureAccess,通过v29指向的0x5000000进行寻址.

获取对应的0x5010000地址指向的指针,这些都是由攻击者控制,因此最终进入0x5010000上由攻击者部署的CLFS!ClfsEarlierLsn地址执行。

从上文中可知代码执行的关键在于0x1570,该值导致container poiner的寻址错误,直接将攻击者构造的other data字段中数据作为container pointer处理,因此我们需要知道0x1570来自何处。

Exp中调用AddLogContainer,对应内核中的函数为CLFS!CClfsLogFcbPhysical::AllocContainer,该函数的this指针指向对象CClfsLogFcbPhysical

CClfsLogFcbPhysical对象0x2b0的位置指向CClfsBaseFilePersisted对象,该对象0x30的位置保存一个指针,该指针指向一段0x90大小的heap内存,这里称之为clfsheap,clfsheap 0x30保存指向base block的指针。

Clfheap可以理解为如下的形式,其保存了各个block的指针,该图出自zscaler的安全研究员针对CVE-2022-37969的分析[3]

base block是一个大小为0x7a00的pool,exp中就是通过该pool的固定大小及clfs标记,通过函数ZQuerySystemInformation在内核中搜索出该pool的地址。

结合上图中base block fffa409cb25e000及clfs的结构可知,trigger clfs中构造的0x68处的0x369对应了record offset array[12],该图出自zscaler的安全研究员针对CVE-2022-37969的分析[3]

而导致0x5000000处调用的0x1570位于base block 0x398的位置,即reContainers,该图出自zscaler的安全研究员针对CVE-2022-37969的分析[3]

因此这里分别对trigger clfs base block这两个偏移下读写断点,如下所示,写断点首先触发,0x398处被写入0x1470。

此时的调用堆栈如下,可以看到还是在AllocContaioner函数中

需要注意的是如果按之前0x1470寻址,最终指向的位置其实是reclinets,而不是导致错误的0x1570指向的rgcontainers。

向下执行到spray[i]触发时的AddLogContainer,其对应的内核函数调用,对应的CClfsLogFcbPhysical/CClfsBaseFilePersisted/base block如下:

再次执行可以看到读断点断下,读取了spray[i] clfs base block + 0x68处的0x369。

紧接着0x369+r15(该值为trigger clfs base block + 0x30),并将该处的数据++,从而触发之前配置的写断点,即将trigger clfs base block 0x398处的0x1470成功修改为0x1570。

此时的调用堆栈如下所示,可以看到依然在AddContainer中。

同理可以看到trigger clfs base中如果按0x1470寻址最后找到的其实是合法的container pointer,而如果按0x1570寻址,最终则指向了攻击者布置的0x5000000。

回到触发写入断点的函数CClfsBaseFilePersisted::WriteMetadataBlock,通过前面的调试可知,触发读写的两个断点直接相邻,且需要注意的是,此时调用AddLogContainer的是spray[i] clfs,即此时CClfsBaseFilePersisted::WriteMetadataBlock函数的this指针应该指向spray[i] clfs 的CClfsBaseFilePersisted对象,而实际上通过spray[i] clfs 的CClfsBaseFilePersisted对象获取的v9的位置却是trigger clfs +0x30,这明显是不符合常理,正因为获取到的v9指向trigger clfs +0x30,从而导致之后poi(poi(trigger clfs + 0x30) + 0x369)++的操作,将trigger clfs +398处的0x1470修改为0x1570。最终导致后续trigger clfsc AddLogContainer调用中寻址contianer ponter错误,进入到0x5000000的攻击者布局内存中。

那这里v9是如何生成的,如上图所示*(_QWORD *)(*((_QWORD *)this + 6) + 24 * v4),取CClfsBaseFilePersisted对象0x30位置的指针+24*v4,该计算中除了v4其余数值都是正常,而v4来自于CClfsBaseFilePersisted::WriteMetadataBlock的第二个参数,同时需要注意的是CClfsBaseFilePersisted对象0x30位置的指针指向的内容是前面的分析中提到,一段长度为0x90的clfsheap,而在这里*(_QWORD *)(*((_QWORD *)this + 6) + 24 * v4),需要计算24*v4,如果v4的值过大将导致heap上的越界读取,这也符合我们一开始总结的exp中spray的内存结构。

start of pipA(trigger clfs + 0x30) ... spray clfs[i] ....pipB(trigger clfs + 0x30)..end of pipA(trigger clfs + 0x30)

至此,我们需要看看这个导致越界的a2来自何处,回到CClfsBaseFilePersisted::WriteMetadataBlock的引用函数CClfsBaseFilePersisted::ExtendMetadataBlock。

可以看到v5来自于CClfsBaseFile::GetControlRecord的第二个参数。

通过ida可以看到CClfsBaseFile::GetControlRecord第二个参数是名为CLFS_CONTROL_RECORD的结构,其生成方式如下所示

CClfsBaseFile::GetControlRecord第一个参数为CClfsBaseFilePersisted,如上述分析,其偏移0x30指向一段长度为0x90大小的clfsheap。

继续往下执行获取clfsheap偏移0x0处的指针,该指针实际对应了clfs的control block,而0x30处就是前面提到的base block。

获取control block偏移0x28处的数值,并和control block相加计算得到返回的CLFS_CONTROL_RECORD

CClfsBaseFilePersisted

+0x30 heap block

0x0 _CLFS_CONTROL_RECORD

CLFS_METADATA_RECORD_HEADER(size 0x70)

这里CLFS_CONTROL_RECORD结构如下所示:

typedef struct _CLFS_CONTROL_RECORD

{

CLFS_METADATA_RECORD_HEADER hdrControlRecord; 70

ULONGLONG ullMagicValue;

UCHAR Version;

CLFS_EXTEND_STATE eExtendState;

USHORT iExtendBlock;

USHORT iFlushBlock;

ULONG cNewBlockSectors;

ULONG cExtendStartSectors;

ULONG cExtendSectors;

CLFS_TRUNCATE_CONTEXT cxTruncate;

USHORT cBlocks;

ULONG cReserved;

CLFS_METADATA_BLOCK rgBlocks[ANYSIZE_ARRAY];

} CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;

可以看到该返回的数据实际是CLFS_CONTROL_RECORD中跳过hdrControlRecord(0x70)之后的位置,该位置偏移0x10开始就是spray[i] clfs构造时设置的数据。

继续向下执行到CClfsBaseFilePersisted::WriteMetadataBlock,此时通过返回的指针寻址到0x1a处的数据,正好就是spray[i] clfs中构造的数据0x13,对应上文CLFS_CONTROL_RECORD结构, 这里ullMagicValue固定为0xc1f5c1f500005f1c,因此这个位置应是iFlushBlock。

这里需要遍历到符合触发结构的spray[i],此时该spray[i]对应的CClfsBaseFilePersisted地址为ffff9087fbd87000。

该spray[i] clfs CClfsBaseFilePersisted对应的clfsheap如下所示:

通过传入的参数2(0x13)计算偏移,最终得出偏移0x1c8,并获取clfsheap+0x1c8处的数据。

但是这里需要注意实际上clfsheap的长度只有0xa0,因此按0x1c8去寻址一定会导致越界读取。

而0x1c8处的数据正好就是我们之前spray时通过pipeB占据写入的数组,而该数组中保存了12个trigger clfs base block +0x30的地址,因此直接越界读取了该数据。

之后代码中通过trigger clfs + 0x30按公式(poi(poi(trigger clfs + 0x30) + 0x369)++)进行运算,导致triger clfs base block原本偏移0x398处的0x1470被修改为0x1570。并最终在triger clfs调用AddLogContainer时,通过0x1570寻址到错误的container poiner,直接执行到攻击者布局的恶意内存0x5000000中。

总结

fun_trigger函数中关键的位置在于修改了spray clfs[i] control block中的对应iFlushBlock,导致之后针对spray clfs[i]调用AddLogContain时CClfsBaseFilePersisted::WriteMetadataBlock超过clfsheap 0x90大小的越界读取。通过spray pip,形成以下内存布局。

start of pipA(trigger clfs + 0x30) ... spray clfs[i] ....pipB(trigger clfs + 0x30)..end of pipA(trigger clfs + 0x30)

越界读取对应spray clfs[i] clfsheap结构后pipB数组中的trigger clfs + 0x30,trigger clfs 中0x58的位置被log初始化时设置为0x369,WriteMetadataBlock继续向下执行,通过越界读取的trigger clfs + 0x30,执行以下代码运算:

poi(trigger clfs + 0x30 + poi(trigger clfs + 0x30 + 0x28))++

这最终导致trigger clfs rgcontainer[0] 中的值由0x1470被修改为0x1570。

之后通过trigger clfs调用CreateLogFile, CClfsBaseFilePersisted::CheckSecureAccess中调用Getsymbol,trigger clfs通过rgcontainer[0]获取对应的container pointer,由于rgcontainer[0]的0x1470已经被修改为0x1570,导致获取的container pointer为攻击者在trigger clfs初始化log时设置的恶意container,其对应的指针为0x5000000。最终eip执行到0x5000000,进入攻击者布局的函数调用链中。

最终的提权样本提供了两种方式,通过在0x5000000上部署以下的函数序列来实现导致任意地址写入

(ClfsEarlierLsn/PoFxProcessorNotification/ClfsMgmtDeregisterManagedClient/SeSetAccessStateGenericMap),任意写入修改了pipe Attribute,通过NtFsControlFileread实现任意地址读取,从而替换当前进程token实现提权。

这里的核心其实是ClfsEarlierLsn和SeSetAccessStateGenericMap。

ClfsEarlierLsn执行完毕后会将rdx赋值为0xffffffff,而该地址上部署了pipe Attributer内核对象。

SeSetAccessStateGenericMap会将rcx+48部署的恶意数据写入到rdx指向的指针中,即pipe Attributer的AttributeValueSize字段,从而可以通过NtFsControlFileread实现任意地址读取。

该利用不像之前CVE-2022-36979简单直接通过ClfsEarlierLsn/SeSetAccessStateGenericMap的组合进行调用,而是在这之间还插入两个函数。首先是PoFxProcessorNotification,该函数会以第一个参数偏移0x68位置为函数指针,偏移0x48为参数进行调用。

插入的第二个函数为ClfsMgmtDeregisterManagedClient,该函数会通过第一个参数偏移8/0x28的位置进行调用,参数本身作为第一个参数,该漏洞利用进入0x5000000的主要调用流程是

PoFxProcessorNotification->ClfsMgmtDeregisterManagedClient,并在ClfsMgmtDeregisterManagedClient中依次调用ClfsEarlierLsn/SeSetAccessStateGenericMap

而实际触发代码执行也是在红框部分,而不是在(**v15)(v15)这里。

样本中第二种提权方式是通过在0x5000000上部署函数序列ClfsMgmtDeregisterManagedClient/RtlClearBit来修改PriviousMod,

最后通过NtWriteVirtualMemory/ NtReadVirtualMemory实现全局内存读写。

补丁对比

补丁中主要对以下两个函数

CClfsBaseFilePersisted::WriteMetadataBlock/CClfsBaseFile::GetControlRecord进行了处理。

首先CClfsBaseFile::GetControlRecord中判断返回的_CLFS_CONTROL_RECORD,防止返回错误的偏移导致越界读取clfsheap。

其次CClfsBaseFilePersisted::WriteMetadataBlock中对返回的v9进行了判断,以防止越界取到攻击者构造的数据。

具体的判断逻辑如下所示

参考链接

[1].https://securelist.com/nokoyawa-ransomware-attacks-with-windows-zero-day/109483/

[2].https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-28252

[3].https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part

[4].https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part2-exploit-analysis

声明:本文来自奇安信威胁情报中心,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。