作者: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
結構,問題就得以解決。