首页
社区
课程
招聘
[原创]CVE-2021-31956分析与利用
发表于: 2022-1-12 00:49 30098

[原创]CVE-2021-31956分析与利用

2022-1-12 00:49
30098

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 被执着的追求编辑 ,原因:
收藏
免费 13
支持
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/03/04 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (3)
雪    币: 1475
活跃值: (14652)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
2
mark
2022-1-12 10:15
0
雪    币: 415
活跃值: (2633)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
厉害得要死。
2022-1-13 16:25
0
雪    币: 11684
活跃值: (7164)
能力值: ( LV13,RANK:550 )
在线值:
发帖
回帖
粉丝
4
https://dawnslab.jd.com/CVE-2021-31956/
2022-1-14 10:57
0
游客
登录 | 注册 方可回帖
返回
//