作者: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 子系統的服務例程,用於根據邏輯筆刷對象在目標表面對象中引擎模擬實現筆刷繪製。根據修復補丁文件對比,發現和其他整數向上溢出漏洞的修復補丁程序類似的,修復這個漏洞的補丁程序也是在函數中對某個變量的數值進行運算時,增加函數 ULongLongToULong
和 ULongAdd
調用來阻止整數向上溢出漏洞的發生,被校驗的目標變量在後續的代碼中被作為分配內存緩衝區函數 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
/ cxPatRealized
和 sizlPat_cy
變量。其中變量 sizlMsk_cx
和 sizlMsk_cy
是參數 psoMask
指向的 SURFOBJ
對象的成員域 sizlBitmap
的值。因此還有 ulSizePat
/ cxPatRealized
和 sizlPat_cy
變量需要繼續向前回溯,以定位出在函數中能夠影響 ulSizeTotal
變量值的最上層可變因素。
可變因素
在 EngRealizeBrush
函數伊始,三個 SURFOBJ
指針參數被用來獲取所屬的 SURFACE
對象指針並分別放置於對應的指針變量中。SURFACE
是內核中所有 GDI 表面對象的管理對象類,類中存在結構體對象成員 SURFOBJ so
用來存儲當前 SURFACE
對象所管理的位圖實體數據的具體信息,在當前系統環境下,成員對象 SURFOBJ so
起始於 SURFACE
對象 +0x10
字節偏移的位置。
隨後,參數 psoPattern
指向的 SURFOBJ
對象的成員域 sizlBitmap
存儲的位圖高度和寬度數值被分別賦值給 sizlPat_cx
和 sizlPat_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
格式的情況就賦值為 1
,32BPP
格式的情況就賦值為 32
,以此類推。
與此同時,函數根據目標位圖 GDI 對象的像素格式對變量 cxPatRealized
進行繼續賦值。根據 IDA 代碼對賦值邏輯進行整理:
-
當目標位圖 GDI 對象的像素格式為
1BPP
時:
如果sizlPat_cx
值為32
/16
/8
其中之一時,變量cxPatRealized
被賦值為32
數值;否則變量cxPatRealized
的值以32
作為初始基數,加上變量sizlPat_cx
的值並以32
對齊。 -
當目標位圖 GDI 對象的像素格式為
4BPP
時:
如果sizlPat_cx
值為8
時,變量cxPatRealized
被賦值為8
數值;否則變量cxPatRealized
的值以8
作為初始基數,加上變量sizlPat_cx
的值並以8
對齊。 -
當目標位圖 GDI 對象的像素格式為
8BPP
/16BPP
/24BPP
其中之一時:
變量cxPatRealized
的值以4
作為初始基數,加上變量sizlPat_cx
的值並以4
對齊。 -
當目標位圖 GDI 對象的像素格式為
32BPP
時:
變量cxPatRealized
被直接賦值為變量sizlPat_cx
的值。
接下來,函數將變量 cxPatRealized
的值與變量 ulSizePat
存儲的目標位圖對象的像素位數相乘並右移 3 比特位,得到圖案位圖新的掃描線的長度,並將數值存儲在 cjScanPat
變量中。
在 Windows 內核中處理位圖像素數據時,通常是以一行作為單位進行的,像素的一行被稱為掃描線,而掃描線的長度就表示的是在位圖數據中向下移動一行所需的字節數。位圖數據掃描線的長度是由位圖像素位類型和位圖寬度決定的,位圖掃描線長度和位圖高度的乘積作為該位圖像素數據緩衝區的大小。
函數隨後計算 cjScanPat
和 sizlPat_cy
的乘積,得到新的圖案位圖像素數據大小,與 0x44
相加並將結果存儲在 ulSizeTotal
變量中。此處的 0x44
是 ENGBRUSH
類對象的大小,將要分配的內存緩衝區頭部將存儲用來管理該筆刷實現實體的 ENGBRUSH
對象。
這裡的新的圖案位圖像素數據大小,是通過與邏輯筆刷關聯的圖案位圖對象的高度和寬度數值,和與設備關聯的目標表面對象的像素位顏色格式數值計算出來的,在函數後續為引擎模擬實現畫刷分配新的位圖表面對象時,該數值將作為新位圖表面對象的像素數據區域的大小。
接下來函數還判斷可選的參數 psoMask
是否為空;如果不為空的話,就取出 psoMask
對象的 sizlBitmap
成員的高度和寬度數值,並依據前面的像素格式為 1BPP
的情況,計算掩碼位圖掃描線的長度和掩碼位圖數據大小,並將數據大小增加進 ulSizeTotal
變量中。
在調用函數 PALLOCMEM
時,傳入的分配內存大小參數是 ulSizeTotal + 0x40
,其中的 0x40
是 ENGBRUSH
結構大小減去其最後一個成員 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
中僅對該函數進行了少量的引用。
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
转载请注明:IAMCOOL » 分析筆記:CVE-2017-0101 整數溢出漏洞