CVE-2021-31956提权漏洞分析与利用

声明

以下内容,来自先知社区的任意门作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。

漏洞介绍

CVE-2021-31956是发生在NTFS.sys中一个提权漏洞,漏洞的成因是因为整形溢出导致绕过条件判断导致的。最后利用起来完成Windows提权

前置知识

在此之前可以大致了解一下关于NTFSNTFS是一个文件系统具备3个功能 错误预警功能,磁盘自我修复功能和日志功能 NTFS是一个日志文件系统,这意味着除了向磁盘中写入信息,该文件系统还会为所发生的所有改变保留一份日志 当用户将硬盘的一个分区格式化为NTFS分区时,就建立了一个NTFS文件系统。

漏洞点分析首先这个函数可以通过ntoskrnl 系统调用来访问,此外还可以控制输出缓冲区的大小,如果扩展属性的大小没有对齐,此函数将计算下一个填充,下一个扩展属性将存储为32位对齐。(每个Ea块都应该被填充为32位对齐) 关于对齐的介绍于计

(padding = ((ea_block_size  + 3) & 0xFFFFFFFC) - e_block_size 

后边用到的结构体typedef struct _FILE_FULL_EA_INFORMATION {ULONG  NextEntryOffset;//下一个同类型结构的偏移,若是左后一个为0UCHAR  Flags;UCHAR  EaNameLength;//eaname数组的长度USHORT EaValueLength;//数组中每个ea值的长度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;

进行函数的部分恢复,这样后续确认漏洞点的话就会比较明显

_QWORD *__fastcall NtfsQueryEaUserEaList(        _QWORD *a1,        FILE_FULL_EA_INFORMATION *eas_blocks_for_file,        __int64 a3,        __int64 User_Buffer,        unsigned int User_Buffer_Length,        FILE_GET_EA_INFORMATION *UserEaList,        char a7){  int v8; // edi  unsigned int v9; // ebx  unsigned int padding; // r15d  FILE_GET_EA_INFORMATION *GetEaInfo; // r12  ULONG NextEntryOffset; // r14d  unsigned __int8 EaNameLength; // r13  FILE_GET_EA_INFORMATION *i; // rbx  unsigned int v15; // ebx  _DWORD *out_buf_pos; // r13  unsigned int ea_block_size; // r14d  unsigned int v18; // ebx  FILE_FULL_EA_INFORMATION *ea_block; // rdx  char v21; // al  ULONG v22; // [rsp+20h] [rbp-38h]  unsigned int ea_block_pos; // [rsp+24h] [rbp-34h] BYREF  _DWORD *v24; // [rsp+28h] [rbp-30h]  struct _STRING DesEaName; // [rsp+30h] [rbp-28h] BYREF  STRING SourceString; // [rsp+40h] [rbp-18h] BYREF  unsigned int occupied_length; // [rsp+A0h] [rbp+48h]  v8 = 0;  *a1 = 0i64;  v24 = 0i64;  v9 = 0;  occupied_length = 0;  padding = 0;  a1[1] = 0i64;  while ( 1 )  {                                             // 创建一个索引放入ealist成员,后续循环取值    GetEaInfo = (FILE_GET_EA_INFORMATION *)((char *)UserEaList + v9);    *(_QWORD *)&DesEaName.Length = 0i64;    DesEaName.Buffer = 0i64;    *(_QWORD *)&SourceString.Length = 0i64;    SourceString.Buffer = 0i64;    DesEaName.Length = GetEaInfo->EaNameLength;    DesEaName.MaximumLength = DesEaName.Length;    DesEaName.Buffer = GetEaInfo->EaName;    RtlUpperString(&DesEaName, &DesEaName);    if ( !(unsigned __int8)NtfsIsEaNameValid(&DesEaName) )      break;    NextEntryOffset = GetEaInfo->NextEntryOffset;    EaNameLength = GetEaInfo->EaNameLength;    v22 = GetEaInfo->NextEntryOffset + v9;    for ( i = UserEaList; ; i = (FILE_GET_EA_INFORMATION *)((char *)i + i->NextEntryOffset) )    {      if ( i == GetEaInfo )      {        v15 = occupied_length;        out_buf_pos = (_DWORD *)(User_Buffer + padding + occupied_length);//   // 分配的内核池        if ( (unsigned __int8)NtfsLocateEaByName(// 通过名字查找EA信息                                eas_blocks_for_file,                                *(unsigned int *)(a3 + 4),                                &DesEaName,                                &ea_block_pos) )        {          ea_block = (FILE_FULL_EA_INFORMATION *)((char *)eas_blocks_for_file + ea_block_pos);          ea_block_size = ea_block->EaValueLength + ea_block->EaNameLength + 9;          if ( ea_block_size <= User_Buffer_Length - padding )// 此处其实有个防止溢出的大小的检查          {            memmove(out_buf_pos, ea_block, ea_block_size);// 缓冲区溢出的漏洞点            *out_buf_pos = 0;            goto LABEL_8;          }        }        else        {          ea_block_size = GetEaInfo->EaNameLength + 9;// 通过名字没查到EA信息走的分支          if ( ea_block_size + padding <= User_Buffer_Length )          {            *out_buf_pos = 0;            *((_BYTE *)out_buf_pos + 4) = 0;            *((_BYTE *)out_buf_pos + 5) = GetEaInfo->EaNameLength;            *((_WORD *)out_buf_pos + 3) = 0;            memmove(out_buf_pos + 2, GetEaInfo->EaName, GetEaInfo->EaNameLength);            SourceString.Length = DesEaName.Length;            SourceString.MaximumLength = DesEaName.Length;            SourceString.Buffer = (PCHAR)(out_buf_pos + 2);            RtlUpperString(&SourceString, &SourceString);            v15 = occupied_length;            *((_BYTE *)out_buf_pos + GetEaInfo->EaNameLength + 8) = 0;LABEL_8:            v18 = ea_block_size + padding + v15;            occupied_length = v18;            if ( !a7 )            {              if ( v24 )                *v24 = (_DWORD)out_buf_pos - (_DWORD)v24;              if ( GetEaInfo->NextEntryOffset )              {                v24 = out_buf_pos;                User_Buffer_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 ( EaNameLength == i->EaNameLength && !memcmp(GetEaInfo->EaName, i->EaName, EaNameLength) )        break;    }    if ( !NextEntryOffset )    {      v18 = occupied_length;      goto LABEL_12;    }LABEL_26:    v9 = v22;  }  a1[1] = v9;  if ( NtfsStatusDebugFlags )    NtfsStatusTraceAndDebugInternal(0i64, 2147483667i64, 919230i64);  *(_DWORD *)a1 = -2147483629;  return a1;}

那么三个参数是如何来的,哪一个是用户态可控的,因为如果ea_block_size可控且User_Buffer_Length可控为0就可以轻松绕过检查,ea_block_size还可以正好导致溢出发生。

NtfsQueryEaUserEaList函数大致会做循环遍历文件的每个NTFS扩展属性(Ea-Extended the attribute index)然后从ea_block复制出来这些buffer到缓冲区 (ea_block_size的值)

ea_block_size的值又是由ea_block决定的(ea_block->EaValueLength + ea_block->EaNameLength + 9)其实最后就是绕过这个检查 具体绕过思考

参考与ncc的计算方法,用数学公式表达一下方便(注:以下是根据代码转换成数学公式,只是个人觉得这么理解第一次比较好理解哈)ea_block_size <= User_Buffer_Length - padding 上边说过是绕过这个条件判断的检查首先假设几个值EaNameLength = x ,EaValueLength = y ,ea_block_size = z ,padding就是padding本身,User_Buffer_Length = f那么首先能根据代码确定几个式子 z = x + y + 9 , 判断条件为 z <= f - padding首先开始第一次循环从数组里取值假设x = 5 ,y = 4 , 所以z = 5 + 4 + 9 = 18 ,padding = 0此时如果 设其值为30(User_Buffer_Length -= ea_block_size + padding)那么f = 30 - z + 0 = 12然后计算padding = ((z + 3)& 0xFFFFFFFC) - z = 2第二次从扩展属性取值,依旧 x = 5, y =4 ,z = 5 + 4 + 9=18此时padding为2 f = 12那么 18 <= 12 - 2 这个条件不成立,这是正常的想进行溢出的流程这是假设其值为30的情况也就是f稍大于z的情况,那么我们假设的值不是30是18呢再来一遍第一次循环取值 x = 5,y = 4 , z = 5 + 4 + 9 =18 不变,padding 依旧是018 <= 18 - 0这时候此时条件是满足,接着往下进行设其值为18 (User_Buffer_Length -= ea_block_size + padding)那么f = 18 - 18 + 0 =0 ,padding计算不变 因为觉得padding的值 z 并没有变化 padding = ((z + 3)& 0xFFFFFFFC) - z = 2我们第二次扩展 x = 5 , y = 99 , z = 5 + 99 + 9 = 113z <= f - padding 也就是 113 <= 0 - 2 ,因为是无符号整数,最后-2就会导致整数溢出从而绕过了这个条件那么超出的大小就会被覆盖到相邻的内存导致溢出

代码中其实可以看见其会不断遍历ea_block数组里边的值,然后再根据FILE_GET_EA_INFORMATION 获取到文件里的EA信息,通过上述的分析我们已经知道如何过掉溢出的检查了

分配的池空间PoolWithTag 到 NtfsQueryEaUserEaList -->User_Buffer --> out_buf_pos 最后memmove触发

漏洞触发利用

了解了漏洞触发点之后,下一步就是验证。
首先需要创建一个文件然后添加EA拓展属性=>NtSetEaFile该函数的第3个参数是一个FILE_FULL_EA_INFORM-ATION结构的缓冲区,用来指定Ea属性的值。所以我们可以利用EA属性来构造PAYLOAD, 然后使用NtQueryEaFile函数来触发NtQueryEaFile

查询一下能对EA 扩展属性进行操作的api记一下这两个ZwQueryEaFile , ZwSetEaFile 分别对应NtSetEaFile , NtQueryEaFile

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,    [in] ULONG Length,    );

WNF是一个通知系统在整个系统中的主要任务就是用于通知,相当于通知中心。它可以在内核模式中使用,也可以在用户态被调用WNF。我们要明白上述的输出缓冲区buffer是从用户空间传入的,同时传入的还有这个缓冲区的长度。

这意味着我们最终会根据缓冲区的大小控制内核空间的大小分配,触发漏洞的话还需要触发如上所述的溢出。我们需要进行堆喷在内核进行我们想要的堆布局。

利用手法是WNF

WNF_STATE_DATA  //用户可以定义的NtCreateWnfStateName  //创建WNF对象实例=>WNF_NAME_INSTANCENtUpdateWnfStateData  //写入数据存放在WNF_STATE_DATANtQueryWnfStateData   //读取写入的数据NtDeleteWnfStateData  //释放Create创建的对象

有限的地址读写
所以首先要通过NtCreateWnfStateName创建一个WNF对象实例要利用漏洞溢出点Ntfs喷出来的堆块去覆盖WNF_STATE_DATA中的DataSize成员和AllocateSize成员。

然后可以利用NtQueryWnfStateData去进行读取,NtUpdateWnfStateData 去进行修改相邻WNF_NAME_INSTANCE数据,但是此时这里完成的有限的地址读写。

任意地址读写
利用相对内存写修改邻近的 WNF_NAME_INSTANCE结构的 StateData指针为任意内存地址,然后就可以通过NtQueryWnfStateData,NtUpdateWnfStateData来实现任意地址读写了。最后可以通过NtDeleteWnfStateData可以释放掉这个对象。

欢迎关注长白山攻防实验室微信公众号定期更新优质文章分享

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容