越狱:V8堆沙箱的两种绕过姿势

admin 2026-06-23 05:24:54 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细解析了绕过V8堆沙箱的两种技术路径:一是利用WebAssembly间接函数表构造任意地址写入原语,通过伪造函数索引实现沙箱外内存写入;二是借助JIT编译机制在代码区直接植入shellcode。两种方法均需先获得沙箱内读写能力,文章提供了完整攻击流程和技术细节,为浏览器安全研究提供实用参考。 综合评分: 85 文章分类: 漏洞分析,WEB安全,二进制安全,红队,实战经验


cover_image

越狱:V8 堆沙箱的两种绕过姿势

幻泉之洲

2026年6月22日 10:18 北京

在小说阅读器读本章

去阅读

黑掉浏览器的第二步,卡在了 V8 堆沙箱这堵墙上。本文拆解两种穿墙术:一是利用 WebAssembly 间接函数表构造任意地址写入原语,二是借助 JIT 代码区直接植入并执行 shellcode。两种方法都需要先在沙箱内搞到一个读写能力,然后才能往外逃。

从 Step 1 说起

如果你还记得上一篇,我们通过类型混淆拿到了 Renderer RCE,串联了 Chrome 沙箱逃逸,最后用 Windows 提权收尾。结尾我提了一嘴下一步要搞什么——还有人记得吗?

直接进入正题。

V8 堆沙箱是什么东西

V8 堆沙箱是 Chromium 在 2021 年左右引入的一套隔离机制。核心思路很简单:把所有可以被 JS 操控的对象关在一个 4GB 的虚拟地址空间里,外面是压缩指针,只有沙箱内的代码才能碰外部内存。打一个比方,就是把攻击者关进笼子,即便你在笼子里为所欲为,也够不到笼子外面的东西。

这套机制主要依赖几个技术:

  • 指针压缩:64 位指针砍成 32 位存,高 32 位通过基址寄存器补回去。省内存,也限制了寻址范围。
  • External Pointer Table:管理指向沙箱外的指针,不让攻击者直接碰外部地址。
  • Caged Heap:所有 V8 堆对象分配在一个固定的 4GB 区域内,mmap 基址随机化选在 4GB 对齐的地址上。

说白了,你得先有笼子里的读写能力,然后再想办法逃出去。

方法一:打 WasmIndirectFunctionTable 的主意

这条路子利用的是 WebAssembly 内部的一个数据结构。我们先看一段简单的 Wasm 代码:

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;

Wasm 主要由 Module、Instance、Table 三个概念构成。Module 是未初始化的原始代码,经过初始化变成一个 Instance。一个 Instance 可以拥有多张 Table。

在 V8 内部,Instance 以 WasmInstanceObject 的形式存在,几个关键字段:

  • tables:管理接收到的 Table 列表。
  • indirect_function_tables:管理注册到 Table 上的 Wasm 函数信息。
  • imported_function_targets:存放从外部传入的函数地址,比如我们 import 进来的 jstimes3

而 Table 对应的是 WasmTableObject,里面有个 dispatch_tables 字段指向 WasmIndirectFunctionTable 对象。

运行示例代码后 dump 出来的对象结构:

DebugPrint: 0x194f0011b5e9: [WasmInstanceObject] in OldSpace

  • tables: 0x194f001cce3d
  • indirect_function_tables: 0x194f001cce49
  • imported_function_targets: 0x194f001ccded

进一步看 WasmIndirectFunctionTable 的内部:

TQ_OBJECT_CONSTRUCTORS_IMPL(WasmIndirectFunctionTable) PRIMITIVE_ACCESSORS(WasmIndirectFunctionTable, sig_ids, uint32_t*, kSigIdsOffset) PRIMITIVE_ACCESSORS(WasmIndirectFunctionTable, targets, Address*, kTargetsOffset)

targets 字段存的是 64 位地址,指向 $f42 和 $f83 这些 Wasm 函数的实际入口。而写入这个字段的方法在 WasmIndirectFunctionTable::Set() 里:

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

如果攻击者能控制 call_target 的值,就能往 targets 指向的内存写任意地址。那 call_target 怎么来的?它通过 GetCallTarget() 返回:

Address WasmInstanceObject::GetCallTarget(uint32_t func_index) { wasm::NativeModule* native_module = module_object().native_module(); if (func_index < module()->num_imported_functions()) { return imported_function_targets().get(func_index); } return jump_table_start() + JumpTableOffset(native_module->module(), func_index); }

逻辑很清楚:如果 func_index 落在 imported 范围内,返回 imported_function_targets 中的值;否则返回 jump_table_start 加个偏移。

这里有个关键点:imported_function_targets 存的是压缩指针,指向沙箱内的地址;而 jump_table_start 是裸指针,指向沙箱外的 RWX 区域。我们还没拿到任意地址写的能力,所以搞不动 jump_table_start,只能打 imported_function_targets 的主意。

但还有个问题:要触发完整的写入流程,目标函数必须是 “exported” 的。而 exported 函数的 func_index 按道理不会落在 imported 范围内。

这就是整个方法最巧妙的地方:通过伪造沙箱内的 WasmExportedFunctionData 对象,把 exported 函数的 func_index 改成 0,让它”骗过”系统去读 imported_function_targets

完整攻击流程

  1. 创建一个 Wasm Table 和一个引用这个 Table 的 Instance。
  2. 破坏 WasmIndirectFunctionTable 里的 targets 字段,让它指向一个攻击者选定的地址。这个地址就是任意地址写(AAW)的 “where”。
  3. 伪造 exported 函数的 function_index 为 0。
  4. 修改 imported_function_targets 指向的内容为任意值。这个值就是 AAW 的 “what”。
  5. 调用 WebAssembly.Table.prototype.set(),触发内部逻辑把 “what” 写进 “where”。

有了任意地址写能力之后,把 shellcode 8 字节一组写进 RWX 页面,然后调过去执行就完事了。

▲ 一个微妙的限制条件:exported 函数的 index 必须在 imported 范围内

方法二:走 JIT 的路

第二个方法更直接,瞄准了 JavaScript 的即时编译(JIT)机制。

JIT 在程序执行的瞬间把代码编译成机器码。Wasm 的 JIT 代码存在 V8 堆外的 RWX 区域,而普通 JS 函数的 JIT 代码存在堆内。

看个简单例子:

const foo = () => { return [1.1, 2.2, 3.3]; } foo(); %DebugPrint(foo);

输出中 code 字段指向一个 BUILTIN InterpreterEntryTrampoline,这是解释器入口。再查那个地址,能看到指令区和 RX 权限。

接着触发优化:

%PrepareFunctionForOptimization(foo); foo(); %OptimizeFunctionOnNextCall(foo); foo(); %DebugPrint(foo);

这次 code 变成了 TURBOFAN,是 Turbofan 编译器生成的机器码。dump 出来的汇编里能看到浮点数的 IEEE 754 表示:

0x1500080b8 98 d2933350 movz x16, #0x999a 0x1500080bc 9c f2b33330 movk x16, #0x9999, lsl #16 0x1500080c0 a0 f2d33330 movk x16, #0x9999, lsl #32 0x1500080c4 a4 f2e7fe30 movk x16, #0x3ff1, lsl #48

这些指令构造的就是 1.1、2.2、3.3 的 double 值。

核心洞察:如果你能在 JS 函数里精确控制浮点数的值,就能在 JIT 编译后的 RX 区域里嵌入任意字节码。然后,把 code 字段劫持指向这个地址,调用函数就相当于跳过去执行你的 shellcode。

这样做的好处是:JIT 代码区完全在沙箱外面,不受任何隔离约束。

小结

两种方法各有侧重:

  • Wasm 间接函数表的方法需要更复杂的布局,但要伪造的对象都在沙箱内,理论上只需要一个沙箱内的读写原语就能完成。
  • JIT的方法更直接,但要求你能在生成 JIT 代码前精确控制浮点数组里的每一个字节,并且需要在沙箱外有稳定的跳转目标。

无论选哪条路,前提都是你已经在沙箱里拿到了读写能力。至于怎么拿这个能力,那是 CVE 本身的事。

下一步,Step 3 会深入拆解 CVE-2023-3079,看一个把沙箱绕过完整串联起来的 PoC。

参考资料

  • Wipeload Step 1:https://hackyboiz.github.io/2026/06/06/OUYA77/Wipeload_step1/en/
  • V8 Heap Sandbox A-to-Z
  • The History of V8 Exploit
  • V8 Sandbox 设计文档:https://v8.dev/blog/sandbox
  • V8 指针压缩:https://v8.dev/blog/pointer-compression
  • V8 堆沙箱机制详解:https://hackyboiz.github.io/2025/10/10/OUYA77/Chrome_part4/en/
  • 沙箱绕过技术细节:https://docs.google.com/document/d/1FM4fQmIhEqPG8uGp5o9A-mnPB5BOeScZYpkHjo0KKA8/edit
  • AAW 与 WasmIndirectFunctionTable 利用:https://theori.io/blog/a-deep-dive-into-v8-sandbox-escape-technique-used-in-in-the-wild-exploit
  • V8 patch 记录:https://chromium.googlesource.com/v8/v8/+/b2a94c9023da70c99223640bf99c203425b42dda
  • Wasm 参考:https://webassembly.org/
  • CVE-2023-2033 的 JIT 利用:https://www.turb0.one/pages/Weaponizing_Chrome_CVE-2023-2033_for_RCE_in_Electron:_Some_Assembly_Required.html
  • DiceCTF Memory Hole:https://mem2019.github.io/jekyll/update/2022/02/06/DiceCTF-Memory-Hole.html

参考资料

[1] https://hackyboiz.github.io/2026/06/21/ji9umi/[Wipeload-Step-2]Prison-Break/EN/


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:幻泉之洲 《越狱:V8 堆沙箱的两种绕过姿势》

评论:0   参与:  0