本文對歷史上的微軟IE 瀏覽器的影響較大0day做了梳理,討論了IE漏洞在攻擊防御技術上的進化,以及記錄了此類漏洞前人們在歷史上遺留下來的對抗經驗和足跡。
在IE瀏覽器攻防已經白熱化的進入到第三個階段的時候筆者才進入到IE瀏覽器攻防方面的研究學習。此時代號’Project Spartan’的微軟的Edge瀏覽器從IE11手中接過windows默認瀏覽器的重擔,使得服役將近20年的IE瀏覽器定格11這個歷史的大版本號上面,自覺對IE瀏覽器漏洞的歷史研究應有一篇簡記,可供后來的初入行的安全研究者有所參考,故成此文,疏漏之處再所難免,敬請指正。
0×01 瀏覽器漏洞研究的前置背景
最近幾年,網絡安全研究的部分重心開始有由PC端向移動端傾斜的趨勢。但是在PC端的安全研究依然以瀏覽器IE/Chrome/Firefox/Spartan,Adobe的Reader/Acrobat/Flash系列作為技術研究深度的展現。當然還有部分用于特定地區定向攻擊的文件格式漏洞也屬于一些黑客著重關注的領域,比如日本比較流行的字處理軟件ichiaro和在韓國地區比較流行的Hancom Office等字處理軟件系統以及WPS等字處理軟件方面的文件格式漏洞。
瀏覽器從誕生之初主要提供簡單的文檔閱讀功能.很少構成網絡安全威脅,但隨著互聯網的高速發展,越來越多的功能集被加入到瀏覽器中。瀏覽器不僅需要像操作系統那樣,為閱讀文檔、觀看電影、欣賞音樂等傳統計算機應用提供基礎,也需要為社交網絡、網絡購物等新興互聯網應用提供支持。瀏覽器在增加功能集的同時,也就帶來了更多的安全問題。而集成捆綁于系統中的IE瀏覽器可以占據市場較大份額,自然也成了眾矢之的。針對IE瀏覽器的攻防軍備競賽也就是在這種情況下拉開了帷幕。
0×02 IE瀏覽器漏洞攻防的幾個時代
1.1 緩沖區溢出和ActiveX控件時代(03年-08年)
03年-08年這是一個階段,這個階段時候的IE漏洞基本是以ActiveX控件造成的漏洞居多以及棧溢出漏洞還有一些簡單的堆溢出漏洞。比如IFRAME標簽的單個超長SRC屬性導致的緩沖區溢出以及類似的棧溢出漏洞。
早一些的階段,常規的Fuzz 方法,無論是基于變形的還是基于生成的,比較適合應用于二進制格式的流數據,特別是那些包含大量C 語言結構類型的文件或網絡協議格式。由于格式解析代碼經常不加檢查的使用數據流數據作為內存操作的參數,單點的畸形往往就足以觸發解析代碼中可能存在的處理漏洞:超長數據導致的緩沖區溢出、畸形數值導致的整數上溢和下溢、畸形索引值導致數組的越界訪問、畸形記數導致過量的內存讀寫操作。其中對于超長數據導致的緩沖區溢出,樣本構造起來相對簡單,傳入超長的值。所以這種類型的漏洞由于發掘起來相對簡單。
對早期的IE 0day 漏洞的歷史簡單做一下梳理,只包含了影響比較大的例子(圖1.1.1,圖1.1.2),有些漏洞可能并不是IE 本身的問題,但是以IE 為最主要的利用渠道。
圖1.1.1
圖1.1.2
1.2 時代關鍵字
防護方: DEP /ASLR / Stack Cookie
攻擊方: 棧溢出/簡單堆溢出/ROP/HeapSpray
A 緩沖區溢出
關于緩沖區溢出也簡單給出一個例子方便理解:
圖1.2.1
我們在VC6.0的編譯器編譯上述程序,執行后結果如下:
圖1.2.2
可以看到main函數中只調用了foo這個函數。但是實際運行中,bar函數也被調用了。
實際上因為foo函數處理不當,并且外部輸入超長,造成了緩沖區溢出后修改了foo()函數的返回地址從而導致程序執行本不應該執行的bar()函數。而如果被覆蓋的返回地址是一串經過精心編碼具有后門功能的shellcode,此時計算機即可被惡意攻擊者控制。
這只是一個簡單的C程序的范例,表現在瀏覽器當中形式上多少有所不同,比方說IE瀏覽器支持的IFRAME標簽,IFRAME標簽的單個超長SRC屬性構造超長數據可能就會導致的緩沖區溢出從而瀏覽器崩潰。
DEP和Security Cookie
從上面的緩沖區溢出我們可以看到,在特定的環境下只要控制輸入的數據超長然后覆蓋掉返回地址,只要此時程序崩潰就很容易感知目標程序存在緩沖區溢出漏洞。然后在修改這串超長數據糅合上恰當的shellcode精確覆蓋返回地址就可以執行我們的惡意代碼。只是留給攻擊者的這樣的大好時光極為短暫。微軟從Windows XP SP2開始提供DEP的支持。DEP全稱是Data Execution Prevention,可以分為硬件的DEP和軟件的DEP。但是目的都是一致的。阻止數據頁上代碼執行。(圖1.2.3)
圖1.2.3
由于數據所在的內存頁被標識為不可執行,即使程序溢出成功轉入shellcode的執行,這個時候CPU就會拋出異常從而阻止惡意shellcode的執行。
從這里也可以看到這一階段的攻擊者只是去覆蓋棧上的返回地址,試圖從棧上將惡意shellcode執行起來。
考慮到攻擊是因為覆蓋返回地址產生的,微軟在VS2008和之后的編譯器加入了一個編譯選項GS。也就是Security Cookie也可以稱為Stack Cookie。
圖1.2.4
可以看到在開始的時候會將一個security_cookie提前寫入到棧中。而在函數返回之前會檢查這個security_cookie是否被篡改。
圖1.2.5
一旦被篡改便會跳轉到異常執行的流程:
(圖1.2.6)
當然這里說的是棧中的情況。在堆溢出中也有相似的防護措施如Header Cookie。
盯上SEH
當攻擊者發現覆蓋4字節返回地址(32位系統)去執行shellcode這種攻擊方法的門檻被抬高之后便又想出了新的方案。覆蓋SEH的Exception handler
其實在二進制的攻防當中所有的努力都是為了獲得哪怕一次控制EIP(RIP)的機會。而SEH恰好符合這個要求可以給攻擊者提供這個機會。SEH存放在棧內,故超長的數據就可能覆蓋掉SEH ,其中將異常處理函數的入口地址更改為shellcode的起始地址。由于溢出后產生的錯誤數據往往會觸發異常,而此時shellcode恰好可以得到一次被EIP指向的機會。
只是留給攻擊者的好時光依然極為短暫。在VS2003(.net)當中支持了/SafeSEH的選項,用于應對針對S E H的攻擊。后來又進一步推出了SEHOP。當然這些措施需要XP SP2操作系統以及更新的操作系統還有DEP的配合。這兩種防護措施詳細展開又要占很多篇幅。感興趣的讀者可以自行學習。
需要說明的是64位的windows系統SEH已經不是放在棧中了。想要通過棧溢出覆蓋異常處理例程來實現漏洞利用已經是不可能的了。
ROP ASLR和HeapSpray技術
前面說到DEP技術即使返回地址被shellcode覆蓋,DEP也會去阻止shellcode的執行。但是如果執行的代碼是操作系統的庫本身提供的函數如直接使用libc庫中提供的system()函數來覆蓋程序函數調用的返回地址。然后傳遞重新設定好的參數使其能夠按照預期執行。這種繞過DEP的攻擊方式稱為return to libc。
Return-to-libc 攻擊用庫函數的地址來覆蓋程序函數調用的返回地址,這樣在程序返回時就可以調用庫函數從而使攻擊得以成功實施。但是由于攻擊者可用的指令序列只能為應用程序中已存在的函數,所以這種攻擊方式的攻擊能力有限。并且攻擊只能在 x86 的 CPU 平臺中實施而對 x86_64 的 CPU 平臺中無效。這是因為x86_64CPU 平臺中程序執行時參數不是通過棧傳遞的而是通過寄存器,而 return-into-libc 需要將參數通過棧來傳遞。
由于這種 return-to-libc 攻擊方式的局限性,返回導向編程(Return-Oriented Programming, ROP)被提出,并成為一種有效的 return-to-libc 攻擊手段。返回導向編程攻擊的方式不再局限于將漏洞程序的控制流跳轉到庫函數中,而是可以利用程序和庫函數中識別并選取的一組指令序列。攻擊者將這些指令序列串連起來,形成攻擊所需要的 shellcode 來從事后續的攻擊行為。因此這種方式仍然不需要注入新的指令到漏洞程序就可以完成任意的操作。同時,它不利用完整的庫函數,因此也不依賴于函數調用時通過堆棧傳遞參數。
一般我們通過immunitydbg配合mona的腳本插件提取rop的指令序列構造rop chain。
Rop chain 展示:
圖1.2.7
Rop的出現一度使得在XP時代的攻擊者占據上風。由于dll加載地址的固定。針對不同的IE版本然后提取rop chain的工作雖然讓人感覺無趣但是達成的攻擊效果的確不錯。但是作為防御一方的微軟的腳步并沒有停止。Vista系統的臃腫繁雜為人所詬病并且市場份額也并不高。但是從Vista系統引入的由Win7沿襲的ASLR機制卻結結實實的又一次提高了攻擊的門檻。
在Rop攻擊中,攻擊者可以事先預知特定的函數如system或者VirtualProtect的入口地址。這是因為在XP以及2000的操作系統上面,由于kernel32.dll這些動態鏈接庫加載地址是固定的,所以導致相關的函數入口地址也是固定的即攻擊者可以事先確定這些函數的入口地址。
當然Rop這一技術有一個弊端就是針對不同的操作系統要編寫提取不同的rop chain。這使得兼容性并不是很好。
ASLR全稱Address space layout randomization,是系統級別的特性,率先在Vista操作系統中得到支持。它的原
理就是在 當一個應 用程序或動態鏈接庫 ,如 kernel32.dll,被加載時,如果其選擇了被ASLR保護 ,那么 系統就會將其加載的基址隨機 設定 。這樣 ,攻擊者就無法事先預知動態庫,如 kernel32.dll的基址,也就 無法事先確定特定 函數,如 VirtualProtect的入 口地址 了。
如果感興趣,可以自己寫一段簡單的C代碼打印出VC運行庫的加載地址。會發現每次重啟之后win7下面VC運行庫加載地址是變化的,但是XP系統VC的運行庫加載地址就是固定的。
Heap Spray
ASLR在新系統上面的應用又使得相當長的一段時間在緩沖區溢出利用時代攻擊方陷入了弱勢。但是攻擊者發現之前很早就被提出的(2001年左右)heap spray正好可以解決這個問題。基本在2005年之后IE漏洞的利用很多都用到了Heap Spray的技術。
在緩沖區溢出的時候,我們能夠劫持覆蓋一個地址,從而使得程序崩潰,但是只使得程序崩潰這樣是沒有價值的接下來如何將程序的執行流程交接到shellcode的手中,這就變成了一個問題。如果覆蓋到一個固定的地址比0x0C0C0C0C,0x0A0A0A0A,0x0D0D0D0D而恰好從這個地址開始布滿了我們的shellcode。這樣觸發漏洞的時候就轉入了我們的shellcode進行了惡意代碼的執行。
實際應用當中shellcode前面都是要加上一些slidecode的(滑板指令)。為什么要加入滑板指令而不是全用shellcode去填充呢。因為如果要想shellcode執行成功,必須要準確命中shellcode的第一條指令,如果整個進程空間都是shellcode,反而精確命中shellcode的概率大大降低了,因為必須要命中第一條指令,加上slidecode之后,現在只要命中slidecode就可以保證shellcode執行成功了,一般shellcode的指令的總長度在50-100個字節左右,而slidecode的長度則大約是100萬字節(按每塊分配1M計算),那么現在命中的概率就接近99.99%了。因為現在命中的是slidecode,那么執行slidecode的結果也不能影響和干擾shellcode。但是如果單純使用0×90(NOP)指令進行填充,因為現在使用較多的攻擊場景是覆蓋虛函數指針(這是一個多級指針),這種情況下如果你使用0×90來做slidecode,而用0x0C0C0C0C去覆蓋虛函數指針,那么現在的虛表(假虛表)里面全是0×90909090,程序跑到0×90909090(內核空間)去執行,直接就crash了。根據這個流程,你可以看到,我們的slidecode也選取0x0C0C0C0C就可以了。
(圖1.2.8)
大概大量分配內存之后分別覆蓋到的地址是這樣的:
0x0A0A0A0A(160M),
0x0C0C0C0C(192M),
0x0D0D0D0D(208M)
網馬里面進行堆噴時,申請的內存大小一般都是200M的原因,主要是為了保證能覆蓋到0x0C0C0C0C地址。(圖1.2.9)
2.1 UAF時代
由于緩沖區類漏洞由于發掘起來相對簡單,在攻防對抗的時間長河中這類漏洞資源很快耗盡。08年之后釋放重利用這樣漏洞利用方式變成了IE漏洞的主流。逐漸在這幾年達到了高峰。對象畸形操作類的漏洞一般來說觸發漏洞需要一系列的操作。單個的操作,比方說對象的創建使用刪除都是正常的。導致問題的是對于對象操作的畸形的組合。由于沒有標準的章法可供參考,基于傳統的溢出類漏洞的發掘手段已經不甚適用。
2.2 對象操作類漏洞原理
跟面向過程的編程語言不同,c++支持多態和繼承。支持這些機制的核心就是虛表。C++的(虛)函數指針位于一個全局數組中,形成虛表。而指向這個虛表的指針(VSTR)一般位于對象實例在內存中開始的4個字節(32位系統).
之后才是類中聲明的數據成員,一般按照聲明的先后順序排列。對于存在多態行為的類,子類的所有實例共享相同的虛表,但區別于父類的虛表。對于某個對象,其調用存在多態行為的某個函數時,會先通過虛表指針得到虛表.再根據函數在虛表中的偏移來得到相應的函數指針,最后才會調用函數。
另外,對象所在的地址一般通過ecx等寄存器傳遞。因此.C++中調用存在多態行為的函數的反匯編代碼類似于如下序列:
(圖2.2.1)
我們以stackexchange上面Polynomial 給出的示范代碼對UAF做一下簡介。
如下例的這樣的一段C++代碼:
圖2.2.2
可以衍生為:
注意,當執行到Account_GetBalance的時候,由于Account_Destroy 函數的執行myAccount指針指向的內存已經是一個不確定的狀態。如果此時能夠可靠的觸發Account_Destroy函數。并且填充一塊精心構造的內容到myAccount指針指向的內存,時機在Account_Destroy執行后Account_GetBalance執行之前。很多情況下這是可能實現的。
Account_Create函數執行之后分配了8個字節的內存。Balance和transactionCount分別占據4個字節,并且返回一個指向他們的指針。這個指針儲存在myAccount變量當中。Account_Destroy釋放了這塊內存,但是myAccount變量依然指向那個8字節的內存。我們將39 05 00 00 01 00 00 00 這8字節內容可靠的進行內存分配。由于舊的8字節內存塊已經被標記釋放,所以內存管理器有很大可能會用新分配的內存去覆蓋掉舊的內存塊到那8個字節已經被釋放的內存。這個時候Account_GetBalance函數被調用了,他會去讀取那8個字節的內存塊,但是實際上那8字節的內存塊已經被我們覆蓋成了
Balance 39 05 00 00 (1337)
transactionCount 01 00 00 00 (1)
所以我們已經越權執行到了下一個函數。
當然具體到IE當中,由于對象繁雜,UAF就更為錯綜復雜。
瀏覽器中跟對象操作類漏洞相關的對象有DOM ,BOM ,JavaScript對象。我們以DOM對象的分配過程為例。
DOM(文檔對象模型)提供了操作HTML/XML文檔的接口。IE瀏覽器中跟DOM實現相關的代碼主要在mshtml dll中。mshtml中的CMarkup類負責構造整個htmI樹結構,其成員函數CreateElement會調用全局的CreateElement函數束構造不同標簽對應的元素。比如
對于每個不同的標簽,IE測覽器內部有不同的CTagDesc結構。這些CTagDesc結構中的其中一項就是對應元素的CreateElement函數指針。因此,全局的CreateElement函數,會根據不同的標簽柬獲得對應的CTagDesc結構,然后再從里面取得對應該標簽的CreateElement函數指針然后call過去進行調用。具體可參看全局CreateElement函數的反匯編代碼,如圖2.2.6所示。
這里有一些小細節,有的時候直接用IDA反匯編如mshtml dll這樣的dll文件的時候沒有找到對應的符號表,可以先使用Symbol Type Viewer這樣的小工具將符號表下載下來放到跟dll同目錄然后再使用IDA對相關的dll文件進行反匯編。
接下來,以CObjectElement為例,介紹其創建過程,其他Elenlent的創建過程類似。CObjectElement的初始化是在成員函數CrcateElement函數中完成的。創建過程如下:先分配內存,然后調用構造函數,最后將返回的對象指針保存在傳入的CElemen**參數中。反匯編代碼如圖。
圖2.2.9
HeapAlloc進行堆內存分配,高版本的一些mshtml dll中可能是由ProcessHeapAllocClear這個函數進行內存的分配。傳給HeapAlloc的字節數是0E0h可知,當前IE瀏覽器版本中的CObjectElement大小為E0h。
接下來調用CObjectEtemem的構造函數完成CObjectElement對象的初始化,構造函數會自動調用父類的構造函數。調用完構造函數后.會將新建的CObjcctElemenl對象指針保存在傳入的參數CElemen**中。這是通過代碼
mov ecx,[ebp+arg_8]
mov[ecx],eax
完成的。
IE瀏覽器采用引用計數束跟蹤DOM對象的生命周期。引用計數(Reference Counting)算法對每個對象計算指向它的指針的數量。當有一個指針指向該對象時計數值加1 ,當刪除一個指向酸對象的指針時,計數值減l。如果計數值減為0,說明不存在指向該對象的指針.此時就可以安全的銷毀潑對象。垃圾回收過程就是回收引用計數為0的對象。引用計數算法的優點是算法實現簡單,并且進行垃圾回收時無需掛起應用程序,回收速度快。
缺點是出于每一次對對象的指針操作都要對對象的引用計數進行更新,因此會減緩系統的整體運行速度。另外,使用引用計數算法的每個對象都需要額外的空問存儲計數值。除此之外,引用計數算法的最大缺點是無法處理循環引用。循環引用指的是兩個對象互相指向對方。此時兩個對象的引用計數都依賴于對方.因此始終無法減至0。
IE瀏覽器實現引用計數的核心就是IUnknown接口。該接口提供了兩個非常重要的特性:生存期控制與接口查詢。對象內部通過引用計數來實現對象的生存期控制。調用程序不甩在意對象的內部實現細節.通過接口查詢即可獲得指向對象的指針。IE瀏覽器中的很多類都繼承于IUnknown。IUnknown有三個方法。
圖2.2.10
以上節介紹的
PrivateQueryInterface會先調用CElement::CreateTearOffThunk函數退回對象包裝后的指針.然后在接下來調用CCaret::AddRef函數(call eCX)增加引用計數。
而CElement::CreateTearOffThunk函數僅僅是簡單的調用全局的CreateTearOflThunk函數。全局的CreateTearOflThunk函數反匯編部分代碼如圖
圖 2.2.11
再來看看釋放引用時所做的工作。對于CElement,用戶不再需要其引用時,調用CElement::Release即可。CElement::Release是對CElement::PrivateRelease的封裝,而CElemem::PrivateRelease主要的工作是調用父類CBase的PrivateRelease函數。CBase::PrivateRelease負責減少引用計數。
實際上IE當中這種對對象的創建和銷毀的場景比比皆是,這也是在緩沖區漏洞在IE上面幾近絕跡后UAF中興的基礎。
2.3 時代關鍵字
Deferred/Delayed Free Control Flow Guard Isolated Heap
上文已經簡單的給出一個例子幫助理解UAF的成因和觸發了。但是由于IE中對象眾多調用關系復雜,微軟作為防守的一方并不能像挖掘緩沖區溢出漏洞一樣容易的窮舉并修復所有潛在的漏洞。但是微軟分別以發布補丁的方式在14年的6月和7月分別引入了隔離堆和延遲釋放的漏洞利用緩解措施。并且在Win8.1Update3和Win10中引入了新的機制Control Flow Guard。我們簡單記錄說明一下這些機制。
UAF的觸發和利用依賴于被釋放的對象的重用。利用的過程必須依賴非法IE對象被確定的分配和釋放。而隔離堆和延遲釋放分別在對象的分配和釋放的時候加入了保護。
在IE中CVE-2014-0282修補前CTextArea::CreateElement分配內存的時候有這樣的代碼
圖2.3.1
漏洞修補之后代碼是這樣的。
圖2.3.2
可以比較明顯的看到存在UAF隱患的對象的內存分配已經單獨使用了隔離堆進行內存分配。
而延遲釋放是這樣的。正常的對象釋放使用HeapFree就立即釋放了,而加入延遲釋放之后需要被釋放的對象會被統一記錄然后根據規則再進行延遲釋放。
再說一下CFG(Control Flow Guard)這個機制。CFG的機制是基于控制流完整性Control-Flow Integrity的設想。這里通過對二進制可執行文件的改寫,對jmp的目的地址前插入一個在改寫時約定好的校驗ID,在jmp的時候看目的地址前的數據是不是我們約定好的校驗ID,如果不是則進入錯誤處理流程。
圖2.3.3
在Call的過程中會引入一個CFG的校驗函數。CFG需要編譯器和操作系統的雙重配合。當這個校驗函數在不支持的操作系統上運行的時候直接就return了。當在被支持的操作系統(win10和win8.1 update3)的時候就會跳轉到一個ntdll里面的一個檢測函數。檢測的機制我們不在詳細展開。
由于在溢出漏洞和UAF的大部分利用當中都依賴于覆蓋某個地址然后劫持程序的EIP跳轉到我們的惡意代碼的地址進行執行。CFG在控制非法地址跳轉方面直接斬斷了大部分漏洞利用的可能。
3.1 后UAF時代
就目前來看,14年之后由于新的緩解措施的加入使得攻防雙方的優勢幾乎一邊的倒向了微軟為首的防守者的陣營。
瀏覽器的漏洞利用已經沒有固定的套路。如瀏覽器內部的腳本引擎的設計錯誤導致從腳本層面突破IE而進行漏洞的相關利用(CVE-2014-6332),對瀏覽器中flash插件的漏洞發掘利用得到ring3權限,然后配合對較老字體解析引擎代碼發掘出來的提權漏洞再進行提權拿到系統權限(Hacking Team相關利用)。漏洞利用方式不一而足,有機會在修訂簡史的時候一并補充。
0×03 IE漏洞防護措施關鍵時點
2015年7月 CFG編譯器支持 VS2015 RTM版本引入/guard開關對Control Flow Guard特性提供編譯器支持。
2014年11月 CFG系統級別支持 Windows8.1 update3 對Control Flow Guard提供系統層面的支持,之后的windows系統均在操作系統層面支持該特性。
2014年7月 MS14-037補丁發布引入Delayed Free特性。
2014年6月 MS14-035補丁發布引入Isolated Heap特性。
2008年1月 SEHOP系統級別支持 發布vista Service Pack 1補丁包,引入對SEHOP特性的操作系統支持。自vista sp1后的windows系統均在操作系統層面支持該特性。
2007年1月 ASLR系統級別支持 windows vista系統引入對ASLR特性操作系統級別的支持。自vista后的windows系統均在操作系統層面支持該特性。
2006年年初 safeseh/stack cookie/aslr/dep編譯器支持 VS2005引入/safeseh編譯開關緩和溢出漏洞對seh的攻擊,引入/GS編譯開關插入Stack Cookie緩和對返回地址的攻擊,加入/dynmicbase編譯開關引入對ASLR特性的編譯器支持,加入/NXCOMPAT編譯開關引入對DE特性的編譯器支持。自VS2005之后的編譯器均支持上述編譯開關。
2004年8月 DEP系統級別支持。微軟推出XP Service Pack 2補丁包引入對DEP特性的操作系統支持,自XP SP2后的windows系統均在操作系統層面支持該特性。