Windows 通用日志文件系统驱动本地权限提升漏洞(CVE-2022-37969 )分析
source link: https://paper.seebug.org/3038/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Windows 通用日志文件系统驱动本地权限提升漏洞(CVE-2022-37969 )分析
2023年09月21日2023年09月21日漏洞分析
原文链接:Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation
译者:知道创宇404实验室翻译组
在本文中,我们将分享CVE-2022-37969的分析内容,并基于Zscaler先前发布的信息构建一个验证PoC。在这里,我们将通过添加详细信息,引导读者深入理解该漏洞,利用它逆向补丁,并创建一个验证PoC。
以下是本文概述:
- 创建初始的BLF日志文件
- 创建多个随机的BLF日志文件
- 制作初始日志文件
- 执行受控的堆喷射
- 准备CreatePipe() / NtFsControlFile()方法
- 一旦内存准备好,将触发漏洞
- 读取系统令牌
- 用系统令牌覆盖我们的进程令牌
- 以系统权限执行进程
- 逆向补丁:分析结构
- 破坏“pContainer”指针
- 重新审视补丁
- 破坏SignatureOffset
- 破坏更多数值
- 控制允许读取SYSTEM令牌的函数
- 自己写一个流程来实现本地提权
- PoC源代码
本文使用的场景是在Windows 11 21H2 (OS Build 22000.918)中完成的,clfs.sys版本为v10.0.22000.918。
创建初始的BLF日志文件
首先,通过使用CreateLogFile()函数,在公共文件夹(%public%)中创建一个名为MyLog.blf的文件:
创建多个随机的BLF日志文件
接下来,它将使用循环创建多个具有随机名称的日志文件。
在循环内部,它调用我们的getBigPoolInfo()函数:
它调用NtQuerySystemInformation(),并将0x42(十进制66)作为第一个参数。它将返回5中有关 bigpool 中进行的 raid 的信息,其结构类型为SYSTEM_BIGPOOL_INFORMATION。
需要调用此函数两次。第一次会返回一个错误,但会给我们提供第二次调用以获取所需信息的正确缓冲区大小。
v5将接收SYSTEM_BIG_POOL_INFORMATION结构的信息。
在bigpool中的分配数量存储在名为Count的第一个字段中。第二个字段中有一个名为SYSTEM_BIGPOOL_ENTRY的结构数组。
从那里开始,我们将在所有结构中搜索包含“Clfs”标记和大小为0x7a00的项。
VirtualAddress存储在一个名为kernelAddrArray的数组中,该数组是具有CLFS标记和大小为0x7a00的每个结构的第一个字段。我们将满足这两个条件的池称为“right pools”。
除了存储数组中的 right pool之外,它还将最后一个找到的right pool存储在a2变量的内容中,该变量用作函数的参数。
这样,a2始终指向具有CLFS标记和大小为0x7a00的right pool。
在调用getBigPoolinfo()之前,变量v26始终存储之前找到的right pool,因为它等于v24 (v26=v24) ,但是当退出此调用时,v24会被更新,而v26保留在前一个right pool。
然后,它对两个方向进行相减,如果结果为负,则反转操作数,使其始终为正。
接着进行类似的操作。在这种情况下,v23最初为零,因此第一次v23=v32。
下一次循环时,v23仍然保持相同的值,不为零,因此会跳出循环并执行以下操作。
V32 还有最后一个区别。如果v32和v23相等,则会输出并加一,但将计数器重置为零。
这个想法是找到六个连续的CLFS标签和大小为0x7a00的比较,它们的差异是相等的,这个差异将是0x11000。执行此操作时,我们将看到当找到六个(因为它从零开始)连续的相等距离时,它将显示它们之间的差异值。
在执行这个操作时,我们将看到找到了六个连续的比较,留下了日志创建文件的循环。
在“public”文件夹中,我们可以看到创建的文件:
制作初始日志文件
我们的craftFile()函数打开原始文件(MyLog.blf)并对其进行修改以触发漏洞。
在修改文件后,有必要更改CRC32,否则我们会收到一个损坏文件的错误消息。
该值位于文件的偏移量0x80C处。
执行受控堆喷射
接下来,它执行 HeapSpray,使用VirtualAlloc()函数在任意地址0x10000和0x5000000分配内存,并在第二次分配 ( 0x10000 ) 中每 0x10 字节保存值0x5000000。
准备CreatePipe() / NtFsControlFile()方法
CreatePipe()用于创建匿名管道,并使用0x11003c作为参数调用NtFsControlFile()以添加属性。稍后可以使用参数0x110038再次调用此函数来读取它。
可以在这里找到该方法的更多详细信息。
接下来我们看到了输入缓冲区,这是我们要添加的属性。如果我们再次使用参数0x11038调用NtFsControlFile(),它应该返回相同的属性。
在池中搜索已创建属性(NpAt)的标签。
找到后,将其保存在v30.Pointer中,这是该池的VirtualAddress。
v30.Pointer+24指向内核池中的AttributeValueSize,并将其保存在我们之前创建的HeapSpray之一中。
这个想法是写入该内核地址+8,以覆盖AttributeValue。
PipeAttribute结构的第一个字段是一个LIST_ENTRY,其大小为16字节。然后它有一个指向属性名称的指针,其大小为8字节。然后它的值为 0x18(十进制 24),这是我们存储在 HeapSpray 中的AttributeValueSize字段。
之后,我们在用户模式中加载CLFS.sys和ntoskrnl。通过使用GetProcAddress(),我们找到ClfsEarlierLsn()和SeSetAccessStateGenericMapping()函数的地址。
然后,我们调用FindKernelModulesBase()函数,该函数将使用NtquerySystemInformation())查找两个相同模块的内核库,这次使用SystemModuleInformation参数返回有关所有模块的信息。
通过这种方式,我们可以计算出每个函数的偏移量,然后在内核中获取它们。
一旦内存准备好,将会触发漏洞
pipeArbitraryWrite()函数被调用两次,有一个标志在第一次调用时初始为零,第二次调用时它的值为1时,它将改变HeapSpray的值。
在0x5000000内存地址的第一次调用中,位于以下值:
请记住,这个值除了在该方向上分配之外,还存储在我们的HeapSpray中。
这是第一次调用后的内存状态,位于0x5000000v左右的地址:
在来自内存0x10000的HeapSpray中,它将在每0x10字节存储指向AttributeValueSize的指针,此外还有指向0x5000000的指针。
读取系统令牌
此序列将触发漏洞:
首先对经过处理的文件调用CreateLogFile(),然后使用随机名称调用另一个文件。然后使用这些文件的句柄调用AddLogContainer()。
NtSetinformationFile ()被调用,且句柄被关闭,指针被损坏。(这将在后面解释。)
HeapSpray 可防止此时发生 BSOD:
在那里设置一个断点,我们可以看到指针已经被破坏,指向我们的HeapSpray,通过它我们可以处理接下来的vtable函数调用。
RAX获取值0x5000000,并首先跳转到位于0x5000000+18的函数,然后跳转到0x5000000+8。
所以第一次跳转是到fnClfsEarlierLsn(),然后到fnSeSetAccessStateGenericMapping()。
从断点开始追踪,我们可以看到它到达了CLFS!ClfsEarlierLsn()。
这个函数被专门调用,因为当它返回时,它将EDX设置为0xFFFFFFFF。
在地址0xFFFFFFFF处,我们存储了SYSTEM EPROCESS & 0xFFFFFFFFFFFFFFF000的结果。
正如我们之前提到的,从CLFS!ClfsEarlierLsn()返回时,RDX的值为0x00000000FFFFFFFF。
然后我们来到了nt!SeSetAccessStateGenericMapping()的第二个函数。
这个函数很有用,因为RCX指向我们的HeapSpray,而我们控制其内容的RDX值是0xFFFFFFFF。
RCX+0x48的内容指向了在v30.Pointer+24中存储的AttributeValueSize的指针值。
AttributeValueSize的指针值被移动到RAX中。然后它读取了地址0xFFFFFFFF的内容,其中存储了 SYSTEM EPROCESS 和 0xFFFFFFFFFFFFFFFF000 的地址。
然后它覆盖了RAX+8中的下一个字段,也就是AttributeValue()。
当然,AttributeValue通常会指向我们在内核中添加的属性。
现在,我们将用SYSTEM EPROCESS & 0xFFFFFFFFFFFFFFF00的结果的指针来覆盖它。
这意味着当我们再次调用NtFsControlFile()函数时(这次使用0x110038参数来读取属性,而不是返回AttributeValue指针指向的"A”),它将从EPRROCESS & 0xFFFFFFFFFFFFFFFFF000中读取请求的字节数,并将其返回到输出缓冲区中,通过这个,在第一次调用时我们可以获取SYSTEM TOKEN的值。
v9b是输出缓冲区的起始地址,其中复制了System EPROCESS & 0xFFFFFFFFFFFFFFF000的结果的内容。
为此,我们将添加 v14,它是System EPROCESS的最后3个字节。然后,0x4b8(该版本的 Windows 11 的Token偏移量)将找到保存 System Token值的该地址的内容。
请记住,最后4位已更改。由于这并不重要,因此该值仍然匹配。
用系统令牌覆盖进程令牌
在第二次调用中,Flag 的值为 1,因为在第一次调用结束时已经递增。
在这里我们可以看到数值被存储的顺序。
我们可以看到地址0xFFFFFFFF,以及我们刚刚找到的系统进程令牌的值。
我进程的Token地址的值存储在HeapSpray中,我们将从中减去8,这个值加上8将被用作目标。请记住,我们写在RAX+8指向的地址上。
以下是从0x5000000开始的内存地址。
我们还可以看到它使用了另一个容器的名称,因为系统进程正在使用的前一个容器无法再次打开或删除。
然后以与第一次尝试相同的方式触发漏洞。
又回到了CLFS!ClfsEarlierLsn()。
接着将RDX设为0xFFFFFFFF。
然后是nt!SeSetAccessStateGenericMapping()。
读取要写入的进程的令牌地址(减8)。
我们可以随后读取SYSTEM TOKEN。
然后写入该进程的 Token 地址(加 8),这就是System Token。
这样,进程就获得了系统令牌。
一旦令牌被写入,我们可以启动一个进程来检查权限。在这种情况下,我们将启动Notepad.exe。
以系统权限执行进程
请记住,这个POC只适用于Windows 11。在Windows 10中,它会产生 BSOD,因此你需要进行一些修改以使其正常工作,但本文未介绍此部分。
逆向补丁:分析结构
我们从IONESCU关于CLFS Internals的优秀工作中获取了CLFS文件格式的结构和大部分文档。
我们可以看到在函数ClfsBaseFilePersisted::LoadContainerQ中添加了一个检查。
执行加法操作的值属于_CLFS_BASE_RECORD_HEADER结构。
请注意,基本块从文件的偏移量0x800开始,到偏移量0x71FF结束,对应于日志块标头的前 0x70 字节。
作为一个好的实践,我们可以在IDA中添加CLF_LOG_BLOCK_HEADER结构:
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
接下来是基本记录头 (CLFS_BASE_RECORD_HEADER),它从文件开头的偏移量0x870开始,长度为0x1338字节。
如果你想将其导入到IDA中,首先你需要添加以下类型和缺失的结构:
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
现在准备添加:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;
在包括这些结构之后,我们注意到在cbSymbolZone和CLFS_BASE_RECORD_HEADER结束的地址(起始地址 + 1338h)之间执行了一个加法操作。
请记住,cbSymbolZone已经在经过处理的日志文件中从0x000000F8修改为了0x0001114B。
0x800(基本块开始的偏移量)+ 0x70(logBlockHeader)+ 0x1328(cbsymbolZone)
0x800 + 0x70 + 0x1328 = 0x1b98
在MyLog.blf 文件中经过处理的cbsymbolZone:
由于补丁位于CClfsBaseFilePersisted::LoadContainerQ函数中,我们必须查看CClfsBaseFilePersisted对象。
在CLFS!CClfsBaseFilePersisted::LoadContainerQ中设置一个断点,并在调用带有经过处理的文件句柄的CreateLogFile时停止。
调用CClfsBaseFile::GetBaseLogRecord函数以获取基本日志记录(CLFS_BASE_RECORD_HEADER)的地址。
RAX将指向CLFS_BASE_RECORD_HEADER的地址。
请注意内存中的CLFS_BASE_RECORD_HEADER结构以及向前0x1328字节的cbsymbolZone字段。
r14存储与“this”对应的结构,即CClfsBaseFilePersisted,因为它是函数CClfsBaseFilePersisted::LoadContainerQ的this。
内存中的CClfsBaseFilePersisted结构:
因此,让我们创建一个长度为0x21c0的结构,同时反转它(这是一个未记录的结构),我们将其称为struct_CClfsBaseFilePersisted。
在函数CClfsBaseFile::GetBaseLogRecord()中获取指向CLFS_BASE_RECORD_HEADER的指针,我们知道该函数中的“this”是结构体struct_CClfsBaseFilePersisted。
读取两个字段(偏移量0x28和0x30)。
字段0x28是一个词,并且其值为6,因此我们将在结构中将其类型更改为word。
目前,我们将其重命名为常量6(const_6)。
根据文档,6 是块的数量CLFS_METADATA_BLOCK_COUNT。该字段可以引用该值。
并且该指针位于偏移量0x30处。
请注意,此处显示的大小包括长度为 0x10 的标头。
当调用ExAllocatePoolWithTag函数时,会请求一些字节,但不包括标头,因此,在调用时将请求0x90字节(0xa0 - 0x10)。
通过搜索文本+30h,写入偏移量0x30的指令有很多,是通过对象CClfsBaseFilePersisted的类型过滤列表我们得到的结果很少,立即找到该大小的分配位置和相同的标记(提示:总是首先查看Create和initialize函数名)。
由于我们仍然不知道名称,我们将其命名为pool_0x90,这是另一个未记录的结构,并创建一个相应大小的结构。
内存中的pool_0x90在其自身偏移0x30处有另一个指针。
这个指针指向文件中的基本块(基本块从偏移0x800开始)。
以下图片来自Zscaler博文:
分配量很大,因为它包含整个基本块。
因此,我们将创建一个新的大小为0x7a00的结构,并将其称为BASE_BLOCK。
前70字节我们已经知道对应于CLFS_LOG_BLOCK_HEADER,接下来的0x1338对应于_CLFS_BASE_RECORD_HEADER。
所以,将Base Block的起始地址与下一个记录的偏移量(即0x70)相加,我们得到了CLFS_BASE_RECORD_HEADER。
内存中的CLFS_BASE_RECORD_HEADER。
查看同一CClfsBaseFilePersisted对象的其他方法,在CClfsBaseFilePersisted::AddContainer中,你会通过CClfsBaseFile::GetBaseLogRecord获得CLFS_BASE_RECORD_HEADER的地址。
接下来,调用CClfsBaseFile::OffsetToAddr并使用cbOffset,它会得到CLFS_CONTAINER_CONTEXT的地址,并将cboffset存储在_CLFS_BASE_RECORD_HEADER的偏移量0x328处的rgbcontainers数组中。
CClfsBaseFile::OffsetToAddr函数用于从偏移量找到结构的地址。
在这一点上,将存储在0x328处的容器偏移量仍然是0,因为我们还没有添加容器。
POC调用CreateLogFile两次,第一次使用格式错误的文件MyLog.blf,第二次使用正常的MyLogxxx.blf文件,所以我们必须在上述所有地方停止调试两次,并在记事本中记录两个文件的上述结构的地址。
我们快进一点到CLFS!CClfsLogFcbPhysical::AllocContainer,在这里设置一个断点并运行。
当POC达到AddLogContainer()时,我们在断点处停止。
让我们还在CClfsBaseFilePersisted::AddContainer+176处设置一个断点,在前面我们看到这将找到CLFS_CONTAINER_CONTEXT结构的偏移量和指针。
当调试器中断时,我们可以看到偏移量是0x1468。
RAX将返回CLFS_CONTAINER_CONTEXT结构的地址。
该结构仍然为空,因为尚未添加容器。
请注意,在我们在格式错误文件的偏移量0x868处写入的SignatureOffset=0x50值,减去基本块开始的0x800,将在偏移0x68处的CLFS_LOG_BLOCK_HEADER结构中。
当POC使用格式错误文件调用AddLogContainer()函数时,在CLFS_LOG_BLOCK_HEADER的偏移0x68处,而不是我们在那里写入的0x50值,内存中当前是0xFFFF0050。
在某个时候,该值被程序修改了,为了查看何时发生了这种情况,在下一次执行中,我们将在写入时设置内存断点。
偏移量存储在r15 + 0x328处(r15指向CLFS_BASE_RECORD_HEADER结构)。
RBX存储偏移量0x1468。
因此,在Base Block地址 + 0x70 + 我们找到的偏移量0x1468,将会是CLFS_CONTAINER_CONTEXT容器的地址。
在CLFS_CONTAINER_CONTEXT结构的偏移0x18处将是pContainer指针,我们可以设置一个写入断点,并查看何时写入。
这是我们必须破坏的指针,因为在漏洞所在的函数中,它首先读取CLFS_CONTAINER_CONTEXT,然后将其移动到r15,接下来读取r15+18的值,这是我们刚刚设置了写入断点的这个指针。
它将pContainer存储在struct_CClfsBaseFilePersisted结构的偏移0x1c0处。
在多次中断后,我们到达了它被损坏的时刻。指针地址的顶部已经从FFs变为零。
当调用格式错误的文件的第二个AddLogContainer()时,会发生这种情况,前一个MyLogxxx的指针已损坏。
出现此问题的原因是SignaturesOffset本来应该是0x50,但现在是0xFFFF0050 ,因此它允许在后面的 memset中越界写入。
损坏“pContainer”指针
memset() 函数将破坏下方的CLFS_CONTAINER_CONTEXT结构,该结构对应于MyLogxxx 文件,因为在创建时,它们彼此相隔0x11000字节。
通过这种方式,它可以准确计算写入下一个结构的位置,并将指针的顶部清零,因此它指向创建 HeapSspray 的用户堆。
调用格式错误的文件的基本块结构仅位于MyLogxxx 文件的前面0x11000字节。
格式错误:
MyLogxxx:
由于添加了0xFFFF0050而不是应该的0x50,所以RCX小于RDX。
然后我们进入memset()函数,用 0 设置 0xb0 字节的数量,RCX 指向MyLogxxx 文件的CLFS_CONTAINER_CONTEXT结构,特别是pContainer的五个高字节。
该指针将因覆盖第一个字节而被损坏:
剩余指向之前我们通过HeapSpray控制的内存地址:
然后, MyLogxxx文件的句柄将被关闭,并到达CClfsBaseFilePersisted::RemoveContainer,最终触发漏洞。
重新审视补丁
现在我们有了更多信息,我们注意到在这里它读取了Base_Block.LOG_BLOCK_HEADER.SignaturesOffset和Base_Block.LOG_BLOCK_HEADER.TotalSectorCount。
在补丁的第一部分中,SignaturesOffset不应大于0x7a00,在我们的版本中,它最初是0x50,如果它到达的值大于 0x7a00,则会将我们抛出。
在已打了补丁的机器上运行 PoC 时,它会将0x50与0x7a00进行比较,因为它更小,所以它会继续执行。
在接下来的块中,格式错误的cbSymbolZone添加到CLFS_BASE_RECORD_HEADER的最终地址的值中,然后将此和存储在result_1中。
然后,将Base_Block的地址与SignatureOffset的值相加,正常文件中的值为0x7980。
base_block的最大地址为0x7a00,现在允许SymbolZone的最大值为在限制之前达到的0x80。
它将把它存储在result_2中,也就是说,SymbolZone在base block内的最大限制将是result_2,然后比较两个结果,如果第一个大于第二个,则意味着它超出了范围。
显然,第一个成员将大于第二个成员,它将不会继续执行,因为cbSymbolZone + CLFS_BASE_RECORD_HEADER的最终地址的第一次求和超过了限制(即result_2),并导致“越界”。
损坏 SignatureOffset
我们需要弄清楚的最后一件事是,SignatureOffset值从0x50变为0xFFFF0050的地方。
因此,让我们重新开始,重新启动并在CLFS!CClfsBaseFilePersisted::LoadContainerQ处停止,其中内存中的值尚未更改,仍然是0x50。
在SignatureOffset的偏移量0x68处设置一个访问断点。
经过几次停止后,我们检测到它修改ClfsEncodeBlockPrivate中的值的正确时刻。
这个函数没有被补丁,所以它可能是由于0x50的低值和其他值的操作导致的行为。
在所构建的值中,我们可以看到ccoffsetArray的值,其在CLFS_BASE_RECORD_HEADER结构中的名称为rgClients,它表示指向 Client Context对象的偏移量数组。
rgClients字段位于CLFS_BASE_RECORD_HEADER结构的偏移量0x138处(0x9a8-0x800-0x70)。
在 PoC 中,该值的格式错误,指向一个名为FakeClientContext的伪造客户端上下文对象。
这是Client Context结构CLFS_CLIENT_CONTEXT:
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
eState值位于结构开始处的偏移量0x78处,在构建的文件中是0x23a0+0x78。
该值显示了日志的状态。
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
此值设置为CLFS_LOG_SHUTDOWN,对应于0x20。
另一个篡改的值是fAttributes,它对应于与基本日志文件(例如 System 和 Hidden)关联的 FILE_ATTRIBUTE标志集。
由于该字段从第0xa个字节开始并跨足两个字节,因此fAttributes的值为0x100。
最后,还有一个指向偏移量0x1bb8的blocknameoffset值,也就是说,通过添加0x78和0x800,它指向文件的偏移量0x2428。
请注意,指向Client Context的偏移量是0x1b30。
所以,Client Context在偏移量0x23a0处。
再往前 0x10个字节,就是对应于blocknameoffset的值。
它将指向字符串名称。最后一个是blockattributeoffset,它在0x2394处的Client Context之前的0xC个字节。
这最后两个值属于一个长度为0x30个字节的CLFSHASHSYM结构的前一个结构,名为CLFSHASHSYM。
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;
它们分别位于CLFSHASHSYM结构的起始处的第0x20和0x24个字节,因此在CLFSHASHSYM结构中,PoC中称blockNameOffset的值实际上是 cbSymName字段,而blockAttributteoffset是cbOffset字段。
这些都是格式错误的值,现在我们需要看看它们是如何影响将我们的SignaturesOffset从0x50值更改为0xFFFF0050。
让我们来看看CClfsBaseFile::AcquireClientContext()函数,它应该返回客户端上下文。
它使用第四个参数调用了CClfsBaseFile::GetSymbol,它将是CLFS_CLIENT_CONTEXT,它将存储指向Client Context的指针。
在 CClfsBaseFile::GetSymbol函数内部,我们将格式错误的ccoffsetArray偏移量传递给CClfsBaseFile::OffsetToAddr,并获取客户端上下文的地址,我们在那里设置一个断点,以便在调用使用CreatelogFile创建的文件时停止。
在那里它被ccoffsetArray精心设计的参数停止。
CClfsBaseFile::OffsetToAddr函数返回了错误的客户端上下文。
并检查cbOffset的值是否为零,因为在 RAX 中找到了CLFS_CLIENT_CONTEXT结构之前的0xC。
然后它将cbOffset与ccoffsetArray(在 RSI 中)进行比较,它们必须相等,否则将会出现错误。
它还检查cbSymName是否等于cbOffset+0x88,如果不是,我们也会收到错误。
最后,它将cidClient字节与零进行比较。
如果所有这些检查都成功,将保存client context。
函数r14的输出指向客户端上下文。
在退出CLfsLogFcbPhysical::Initialize时,我们将拥有CLFS_CLIENT_CONTEXT的地址。
接下来,它读取了fAttributes (0x100)的值。
此函数属于CClfsLogFcbPhysical类。
它被分配在这里,大小是0x15d0,标签是“ ClfC ”。
创建一个结构来存储我们要逆向的内容,我们将其命名为:struct_CClfsLogFcbPhysical。
请注意,在0x2b0处保存了CClfsBaseFilePersisted结构的地址。
在结构中保存了许多值后,它进入了一个重要的部分,使用0x20测试了eState。
由于设计的值为0x20,所以测试将返回 1。
我们可以看到,在构造函数中的vtable是
此处它将检查文件是否为多路复用。
然后,它按预期路径继续执行,进入到CClfsLogFcbPhysical::ResetLog函数。
在该函数中,除了一个初始化为 0xFFFFFFFF00000000 的字段外,几个字段都初始化为零。
这里检索Client Context
它存储值0xFFFFFFFF00000000
它在偏移量0x5c 处写入0xFFFFFFFF ,这是CLFS_LSN lsnRestart.ullOffset的高位部分。
接下来,执行ClfsEncodeBlockPrivate()函数,这个函数负责将0x50覆盖为0xFFFF0050,正如前面所展示的。
在这里,它读取了SignatureOffset = 0x50的值,然后将其添加到CLFS_LOG_BLOCK_HEADER的开头。
这里有一个循环,每次写入两个字节,类似于SignatureOffset,但是不是指向正常文件中的正确值,例如0x3f8,使其向前写入,而是写入到相同的CLFS_LOG_BLOCK_HEADER中。
其目的是改变写入的目标,试图损坏SignatureOffset的值。
普通文件:
此时,它将开始循环并写入两个字节。
计数器必须达到0x3d才能退出循环。
RCX正在从0x200增加,我们已经进入第三个周期,它的值是0x600。
在0xe迭代中,RCX是0x1a00。
那就是它写入0xFFFFFFFF000000的地方。
它正在读取最后的两个字节FFFF。
然后将其复制到R8中。
正如我们前面所看到的,这个值非常关键,因为它允许绕过检查并越界写入,以破坏memset()之后的文件的pContainer指针,然后在memset()中写入零,将其指向我们控制的内存(HeapSpray)。
CbSymbolZone= 0x1114B
添加到CLFS_BASE_RECORD_HEADER最终地址的格式错误的值将使其写入越界,而比较的另一个成员(Base Block + SignatureOffset的地址)仍然是SignatureOffset =0xFFFF0050,允许此检查通过,在memset()中写入越界,并将仍指向 HeapSpray 的指针顶部归零。
由于RCX小于RDX
正如我们之前所看到的(值可能会有所不同,因为它们属于先前的执行)。
它将破坏指针,将最高字节设置为0
使其指向我们通过HeapSpray控制的内存区域。
因此,在触发漏洞时,我们到达了CClfsBaseFilePersisted::RemoveContainer。
将会存在已经损坏的指针,并且可以像我们之前看到的那样被利用。
此时,我们已经成功利用了漏洞,从而控制了允许读取SYSTEM令牌并写入我们自己进程的函数,从而实现了本地特权升级。点击Fortra’s GitHub可以找到验证PoC。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3038/
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK