文章总结: 本文详细解析了绕过V8堆沙箱的两种技术路径:一是利用WebAssembly间接函数表构造任意地址写入原语,通过伪造函数索引实现沙箱外内存写入;二是借助JIT编译机制在代码区直接植入shellcode。两种方法均需先获得沙箱内读写能力,文章提供了完整攻击流程和技术细节,为浏览器安全研究提供实用参考。 综合评分: 85 文章分类: 漏洞分析,WEB安全,二进制安全,红队,实战经验
越狱: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。
完整攻击流程
- 创建一个 Wasm Table 和一个引用这个 Table 的 Instance。
- 破坏
WasmIndirectFunctionTable里的targets字段,让它指向一个攻击者选定的地址。这个地址就是任意地址写(AAW)的 “where”。 - 伪造 exported 函数的
function_index为 0。 - 修改
imported_function_targets指向的内容为任意值。这个值就是 AAW 的 “what”。 - 调用
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 堆沙箱的两种绕过姿势》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论