综述
该在野0day提权漏洞已被Nokoyawa 勒索团伙使用,以用于部署勒索软件前获取目标系统的system权限。
Microsoft 在四月补丁日修复该漏洞[2],并将其标记为CVE-2023-28252(Windows 通用日志文件系统驱动程序特权提升漏洞)。下图是在打补丁前系统上的运行截图,通过漏洞利用完成提权。
漏洞样本分析
这里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类似,但是,该工具链中增加了函数:
ClfsMgmtDeregisterManagedClient
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-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
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)
漏洞原理分析
针对函数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的攻击者布局内存中。
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)
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进行了判断,以防止越界取到攻击者构造的数据。
具体的判断逻辑如下所示
参考链接
[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
点击阅读原文至ALPHA 6.0
即刻助力威胁研判