声明
以下内容,来自先知社区的任意门作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。
漏洞介绍
CVE-2021-31956是发生在NTFS.sys中一个提权漏洞,漏洞的成因是因为整形溢出导致绕过条件判断导致的。最后利用起来完成Windows提权
前置知识
在此之前可以大致了解一下关于NTFSNTFS是一个文件系统具备3个功能 错误预警功能,磁盘自我修复功能和日志功能 NTFS是一个日志文件系统,这意味着除了向磁盘中写入信息,该文件系统还会为所发生的所有改变保留一份日志 当用户将硬盘的一个分区格式化为NTFS分区时,就建立了一个NTFS文件系统。

漏洞点分析首先这个函数可以通过ntoskrnl 系统调用来访问,此外还可以控制输出缓冲区的大小,如果扩展属性的大小没有对齐,此函数将计算下一个填充,下一个扩展属性将存储为32位对齐。(每个Ea块都应该被填充为32位对齐) 关于对齐的介绍于计
(padding = ((ea_block_size + 3) & 0xFFFFFFFC) - e_block_size 
后边用到的结构体typedef struct _FILE_FULL_EA_INFORMATION {ULONG NextEntryOffset;//下一个同类型结构的偏移,若是左后一个为0UCHAR Flags;UCHAR EaNameLength;//eaname数组的长度USHORT EaValueLength;//数组中每个ea值的长度CHAR EaName[1];} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;typedef struct _FILE_GET_EA_INFORMATION {ULONG NextEntryOffset;UCHAR EaNameLength;CHAR EaName[1];} FILE_GET_EA_INFORMATION, * PFILE_GET_EA_INFORMATION;
进行函数的部分恢复,这样后续确认漏洞点的话就会比较明显
_QWORD *__fastcall NtfsQueryEaUserEaList(_QWORD *a1,FILE_FULL_EA_INFORMATION *eas_blocks_for_file,__int64 a3,__int64 User_Buffer,unsigned int User_Buffer_Length,FILE_GET_EA_INFORMATION *UserEaList,char a7){int v8; // ediunsigned int v9; // ebxunsigned int padding; // r15dFILE_GET_EA_INFORMATION *GetEaInfo; // r12ULONG NextEntryOffset; // r14dunsigned __int8 EaNameLength; // r13FILE_GET_EA_INFORMATION *i; // rbxunsigned int v15; // ebx_DWORD *out_buf_pos; // r13unsigned int ea_block_size; // r14dunsigned int v18; // ebxFILE_FULL_EA_INFORMATION *ea_block; // rdxchar v21; // alULONG v22; // [rsp+20h] [rbp-38h]unsigned int ea_block_pos; // [rsp+24h] [rbp-34h] BYREF_DWORD *v24; // [rsp+28h] [rbp-30h]struct _STRING DesEaName; // [rsp+30h] [rbp-28h] BYREFSTRING SourceString; // [rsp+40h] [rbp-18h] BYREFunsigned int occupied_length; // [rsp+A0h] [rbp+48h]v8 = 0;*a1 = 0i64;v24 = 0i64;v9 = 0;occupied_length = 0;padding = 0;a1[1] = 0i64;while ( 1 ){ // 创建一个索引放入ealist成员,后续循环取值GetEaInfo = (FILE_GET_EA_INFORMATION *)((char *)UserEaList + v9);*(_QWORD *)&DesEaName.Length = 0i64;DesEaName.Buffer = 0i64;*(_QWORD *)&SourceString.Length = 0i64;SourceString.Buffer = 0i64;DesEaName.Length = GetEaInfo->EaNameLength;DesEaName.MaximumLength = DesEaName.Length;DesEaName.Buffer = GetEaInfo->EaName;RtlUpperString(&DesEaName, &DesEaName);if ( !(unsigned __int8)NtfsIsEaNameValid(&DesEaName) )break;NextEntryOffset = GetEaInfo->NextEntryOffset;EaNameLength = GetEaInfo->EaNameLength;v22 = GetEaInfo->NextEntryOffset + v9;for ( i = UserEaList; ; i = (FILE_GET_EA_INFORMATION *)((char *)i + i->NextEntryOffset) ){if ( i == GetEaInfo ){v15 = occupied_length;out_buf_pos = (_DWORD *)(User_Buffer + padding + occupied_length);// // 分配的内核池if ( (unsigned __int8)NtfsLocateEaByName(// 通过名字查找EA信息eas_blocks_for_file,*(unsigned int *)(a3 + 4),&DesEaName,&ea_block_pos) ){ea_block = (FILE_FULL_EA_INFORMATION *)((char *)eas_blocks_for_file + ea_block_pos);ea_block_size = ea_block->EaValueLength + ea_block->EaNameLength + 9;if ( ea_block_size <= User_Buffer_Length - padding )// 此处其实有个防止溢出的大小的检查{memmove(out_buf_pos, ea_block, ea_block_size);// 缓冲区溢出的漏洞点*out_buf_pos = 0;goto LABEL_8;}}else{ea_block_size = GetEaInfo->EaNameLength + 9;// 通过名字没查到EA信息走的分支if ( ea_block_size + padding <= User_Buffer_Length ){*out_buf_pos = 0;*((_BYTE *)out_buf_pos + 4) = 0;*((_BYTE *)out_buf_pos + 5) = GetEaInfo->EaNameLength;*((_WORD *)out_buf_pos + 3) = 0;memmove(out_buf_pos + 2, GetEaInfo->EaName, GetEaInfo->EaNameLength);SourceString.Length = DesEaName.Length;SourceString.MaximumLength = DesEaName.Length;SourceString.Buffer = (PCHAR)(out_buf_pos + 2);RtlUpperString(&SourceString, &SourceString);v15 = occupied_length;*((_BYTE *)out_buf_pos + GetEaInfo->EaNameLength + 8) = 0;LABEL_8:v18 = ea_block_size + padding + v15;occupied_length = v18;if ( !a7 ){if ( v24 )*v24 = (_DWORD)out_buf_pos - (_DWORD)v24;if ( GetEaInfo->NextEntryOffset ){v24 = out_buf_pos;User_Buffer_Length -= ea_block_size + padding;padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;// padding对齐的计算goto LABEL_26;}}LABEL_12:a1[1] = v18;LABEL_13:*(_DWORD *)a1 = v8;return a1;}}v21 = NtfsStatusDebugFlags;a1[1] = 0i64;if ( v21 )NtfsStatusTraceAndDebugInternal(0i64, 2147483653i64, 919406i64);v8 = -2147483643;goto LABEL_13;}if ( EaNameLength == i->EaNameLength && !memcmp(GetEaInfo->EaName, i->EaName, EaNameLength) )break;}if ( !NextEntryOffset ){v18 = occupied_length;goto LABEL_12;}LABEL_26:v9 = v22;}a1[1] = v9;if ( NtfsStatusDebugFlags )NtfsStatusTraceAndDebugInternal(0i64, 2147483667i64, 919230i64);*(_DWORD *)a1 = -2147483629;return a1;}


那么三个参数是如何来的,哪一个是用户态可控的,因为如果ea_block_size可控且User_Buffer_Length可控为0就可以轻松绕过检查,ea_block_size还可以正好导致溢出发生。
NtfsQueryEaUserEaList函数大致会做循环遍历文件的每个NTFS扩展属性(Ea-Extended the attribute index)然后从ea_block复制出来这些buffer到缓冲区 (ea_block_size的值)
ea_block_size的值又是由ea_block决定的(ea_block->EaValueLength + ea_block->EaNameLength + 9)其实最后就是绕过这个检查 具体绕过思考
参考与ncc的计算方法,用数学公式表达一下方便(注:以下是根据代码转换成数学公式,只是个人觉得这么理解第一次比较好理解哈)ea_block_size <= User_Buffer_Length - padding 上边说过是绕过这个条件判断的检查首先假设几个值EaNameLength = x ,EaValueLength = y ,ea_block_size = z ,padding就是padding本身,User_Buffer_Length = f那么首先能根据代码确定几个式子 z = x + y + 9 , 判断条件为 z <= f - padding首先开始第一次循环从数组里取值假设x = 5 ,y = 4 , 所以z = 5 + 4 + 9 = 18 ,padding = 0此时如果 设其值为30(User_Buffer_Length -= ea_block_size + padding)那么f = 30 - z + 0 = 12然后计算padding = ((z + 3)& 0xFFFFFFFC) - z = 2第二次从扩展属性取值,依旧 x = 5, y =4 ,z = 5 + 4 + 9=18此时padding为2 f = 12那么 18 <= 12 - 2 这个条件不成立,这是正常的想进行溢出的流程这是假设其值为30的情况也就是f稍大于z的情况,那么我们假设的值不是30是18呢再来一遍第一次循环取值 x = 5,y = 4 , z = 5 + 4 + 9 =18 不变,padding 依旧是018 <= 18 - 0这时候此时条件是满足,接着往下进行设其值为18(User_Buffer_Length -= ea_block_size + padding)那么f = 18 - 18 + 0 =0 ,padding计算不变 因为觉得padding的值 z 并没有变化padding = ((z + 3)& 0xFFFFFFFC) - z = 2我们第二次扩展 x = 5 , y = 99 , z = 5 + 99 + 9 = 113z <= f - padding 也就是 113 <= 0 - 2 ,因为是无符号整数,最后-2就会导致整数溢出从而绕过了这个条件那么超出的大小就会被覆盖到相邻的内存导致溢出
代码中其实可以看见其会不断遍历ea_block数组里边的值,然后再根据FILE_GET_EA_INFORMATION 获取到文件里的EA信息,通过上述的分析我们已经知道如何过掉溢出的检查了

分配的池空间PoolWithTag 到 NtfsQueryEaUserEaList -->User_Buffer --> out_buf_pos 最后memmove触发

漏洞触发利用
了解了漏洞触发点之后,下一步就是验证。
首先需要创建一个文件然后添加EA拓展属性=>NtSetEaFile该函数的第3个参数是一个FILE_FULL_EA_INFORM-ATION结构的缓冲区,用来指定Ea属性的值。所以我们可以利用EA属性来构造PAYLOAD, 然后使用NtQueryEaFile函数来触发NtQueryEaFile
查询一下能对EA 扩展属性进行操作的api记一下这两个ZwQueryEaFile , ZwSetEaFile 分别对应NtSetEaFile , NtQueryEaFile
NTSTATUS ZwQueryEaFile([in] HANDLE FileHandle, //文件句柄[out] PIO_STATUS_BLOCK IoStatusBlock,[out] PVOID Buffer, //扩展属性缓冲区(FILE_FULL_EA_INFORMATION结构)[in] ULONG Length, //缓冲区大小[in] BOOLEAN ReturnSingleEntry,[in, optional] PVOID EaList, //指定需要查询的扩展属性[in] ULONG EaListLength,[in, optional] PULONG EaIndex, //指定需要查询的起始索引[in] BOOLEAN RestartScan);NTSTATUS ZwSetEaFile([in] HANDLE FileHandle,[out] PIO_STATUS_BLOCK IoStatusBlock,[in] PVOID Buffer,[in] ULONG Length,);


WNF是一个通知系统在整个系统中的主要任务就是用于通知,相当于通知中心。它可以在内核模式中使用,也可以在用户态被调用WNF。我们要明白上述的输出缓冲区buffer是从用户空间传入的,同时传入的还有这个缓冲区的长度。
这意味着我们最终会根据缓冲区的大小控制内核空间的大小分配,触发漏洞的话还需要触发如上所述的溢出。我们需要进行堆喷在内核进行我们想要的堆布局。
利用手法是WNF
WNF_STATE_DATA //用户可以定义的NtCreateWnfStateName //创建WNF对象实例=>WNF_NAME_INSTANCENtUpdateWnfStateData //写入数据存放在WNF_STATE_DATANtQueryWnfStateData //读取写入的数据NtDeleteWnfStateData //释放Create创建的对象
有限的地址读写
所以首先要通过NtCreateWnfStateName创建一个WNF对象实例要利用漏洞溢出点Ntfs喷出来的堆块去覆盖WNF_STATE_DATA中的DataSize成员和AllocateSize成员。
然后可以利用NtQueryWnfStateData去进行读取,NtUpdateWnfStateData 去进行修改相邻WNF_NAME_INSTANCE数据,但是此时这里完成的有限的地址读写。
任意地址读写
利用相对内存写修改邻近的 WNF_NAME_INSTANCE结构的 StateData指针为任意内存地址,然后就可以通过NtQueryWnfStateData,NtUpdateWnfStateData来实现任意地址读写了。最后可以通过NtDeleteWnfStateData可以释放掉这个对象。

欢迎关注长白山攻防实验室微信公众号定期更新优质文章分享