原文链接:Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation
译者:知道创宇404实验室翻译组
在本文中,我们将分享CVE-2022-37969的分析内容,并基于Zscaler先前发布的信息构建一个验证PoC。在这里,我们将通过添加详细信息,引导读者深入理解该漏洞,利用它逆向补丁,并创建一个验证PoC。
以下是本文概述:
本文使用的场景是在Windows 11 21H2 (OS Build 22000.918)中完成的,clfs.sys版本为v10.0.22000.918。
首先,通过使用CreateLogFile()函数,在公共文件夹(%public%)中创建一个名为MyLog.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()用于创建匿名管道,并使用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中越界写入。
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值从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/