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可以釋放掉這個對象。

歡迎關注長白山攻防實驗室微信公眾號定期更新優質文章分享

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容