V8 沙箱绕过
V8 沙箱绕过
这是 DiceCTF2022 的一道题 memory hole。
题目给了我们修改任意 array 的 length 的能力,按过往的经验,接下来很简单,就是构造任意地址读写原语,构造 WASM 实例,读 RWX 空间地址,写 shellcode ,调 WASM 函数,结束。
但题目开启了 V8 沙箱,一个新的安全机制,直接阻止了我们构造任意地址读写,能访问的范围是 array 基址后连续的 4G 地址空间。
绕过这个沙箱是本题的重点,看了两篇wp有所收获,所以整理了下绕过手法,未来可能会用到。
【题目地址】 https://github.com/Jayl1n/CTF-Writeup/blob/master/DiceCTF2022/memory-hole/1984.tar.gz
指针压缩
64 位 V8 中使用了“指针压缩”的技术,即将 64 位指针转为 js_base + offset 的形式,只在内存当中存储 offset ,寄存器 $14 存储 js_base ,其中 offset 是 32 位的。JS 对象在解引用时,会从 $r14 + offset 的地址加载。因此 js_base + offset 被限制在很小的一个区域,无法访问任意地址。
如下,没有开启“指针压缩”的 ArrayBuffer 内存布局:

开启后:
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 1.png)
绕过“指针压缩”的方法很简单,因为“指针压缩”只对堆上指针使用,堆外指针不会压缩。ArrayBuffer 的 BackingStore 是个堆外指针,可以直接修改 BackingStore 为任意地址进而实现任意地址读写。
V8 沙箱
V8 沙箱扩展“指针压缩”将 V8 堆上的所有原始指针都 “沙盒化”,比如 WebAssembly 的 RWX 页指针和 ArrayBuffer 的 BackingStore 指针。将这些外部指针都转为表的索引,以基址+偏移的方式访问,限制指针能访问的范围,防止攻击者利用 V8 漏洞实现内存任意地址读写。
V8 Sandbox - High-Level Design Doc
如下,未开启 V8 沙箱时的 ArrayBuffer 对象内存布局:
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 2.png)
开启沙箱后,BackingStore 替换为 0x45c00000000(偏移量 0x45c00,向左移动 24 位保证最高位为 0)。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 3.png)
此时假设攻击者能从多个线程中任意破坏沙箱内的内存,现在需要一个额外的漏洞破坏沙箱外部的内存,从而执行任意代码。
绕过
方法一:利用立即数写 shellcode
(参考 https://mem2019.github.io/jekyll/update/2022/02/06/DiceCTF-Memory-Hole.html)
JSFunction
先 DebugPrint 一个 JSFunction 的内存结构:
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 4.png)
这里有一个 code 字段,它指向了函数要执行的汇编指令,处于 r-x 页。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 5.png)
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 6.png)
用 gdb 修改 code 字段 0x41414141 。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 7.png)
继续执行,出现异常,此时 rcx 是 0x2a0c41414141 ,即基址(0x2a0c00000000)+偏移(0x41414141)。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 8.png)
看这段汇编,如果我们令[rcx + 0x1b] & 0x20000000 = 0 ,rip 就会在之后被设置为 rcx+0x3f ,从而劫持 rip ,这个条件是比较容易满足的。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 9.png)
使用立即数构造 shellcode
JS 函数的 JIT 代码存储在堆内,即基址开头的 32 位区域,如下,基址都是 0x350f00000000 。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 10.png)
这个函数返回的是一个浮点数组,在汇编里,每个浮点数以立即数的形式存在,立即数占 8 个字节。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 11.png)
立即数同样可以被识别为汇编指令,很容易想到可以利用这个立即数来布置 shellcode,只要将 shellcode 片段用 jmp 连接起来,就能将一个个立即数串联起来,实现完整的功能。
jmp 短跳需要 2 个字节,剩下 6 个字节可以自由发挥。
参考原作的脚本生成 shellcode,再将输出转为 IEEE 浮点表示。
1 | from pwn import * |
生成出来的 shellcode 是通过系统调用执行 /bin/sh 。
跟一下
1 | gef➤ job 0x3de400045681 |
以指令格式查看这几个立即数,可以看到这几个立即数是通过 jmp 串联起来了。
1 | gef➤ x/3i 0x3de40004573c |
执行
接下来就是劫持 rip 。
修改 JSFunction 对象的 code 字段,令 code + 0x3f = 0x3de40004573c 。
code 的计算方式 0x3de400045681 + (0x3de40004573c - 0x3f - 0x3de400045681) = 0x3de400045681 + 0x7c ,即原 code 值加 0x7c ,具体各位自行体会,原作的 jitAddr + 0xb3 - 0x3f 的计算在我这跑不起来,差了 8 个字节,不知道是不是环境问题。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 12.png)
EXP
1 | function dp(x) {}// %DebugPrint(x);} |
方法二:利用 WasmInstance 的全局变量
(参考:https://blog.kylebot.net/2022/02/06/DiceCTF-2022-memory-hole/)
尽管沙箱几乎把所有指针都压缩了,但依然存在一些64位的原始指针,可以尝试劫持它们来绕过沙箱。
全局变量
WasmInstance 对象的 imported_mutable_globals 存储 WASM 代码中使用的所有全局变量,它并没有被沙箱保护起来。
下面是一个 WasmInstance 对象:
1 | DebugPrint: 0x3b17081d2f3d: [WasmInstanceObject] in OldSpace |
查看内存,imported_mutable_globals 确实还是64位。
1 | gef➤ x/20xg 0x3b17081d2f3d-1 |
使用全局变量
1 | var global = new WebAssembly.Global({value:'i64', mutable:true}, 0n); |
以上可以往 imported_mutable_globals 里添加一个 int64 的全局变量 。
注意global 这个变量是在当前堆上分配的,利用漏洞是可以修改这个对象的属性。
DebugPrint 一下这个 global
1 | DebugPrint: 0xc7908048d0d: [WasmGlobalObject] |
untagged_buffer 是一个 ArrayBuffer,backing_store 是 0x3b1800002000 ,也就是 global 存储数据的地址。
1 | gef➤ job 0x3b1708048d31 |
回过头看上面 wasm_instance 的 imported_mutable_globals
1 | DebugPrint: 0x3b17081d2f3d: [WasmInstanceObject] in OldSpace |
这里的第一个元素即是 global 的 backing_store 地址
1 | gef➤ x/10xg 0x560e9be53770 |
我们伪造一个 imported_mutable_globals 替换掉 wasm_instance 的 imported_mutable_globals ,即可做到任意地址读写。
伪造 imported_mutable_globals
imported_mutable_globals 并不是一个 JS 对象,不用泄漏 map ,伪造起来比较容易。
创建一个 array ,第一个元素是要读写的任意地址。
再泄漏这个 array 的偏移及基址 js_base 计算得到完整的 array 地址,覆盖掉用来的 imported_mutable_globals 。
泄漏 array 的偏移按常规的路子来就行,泄漏 js_base 见下一节。
一切搞好后,要读写任意地址,改 array[0] 即可。
获取基址 js_base
泄漏基址 js_base 并不难,多次运行 d8 ,搜索下基址:
第一次
1 | gef➤ search-pattern 0x1c53 |
第二次
1 | gef➤ search-pattern 0x00002c3b |
第三次
1 | gef➤ search-pattern 0x3f13 |
可以看到,在 [js_base , js_base+0x3000] 的区间就有一些64位的原始指针,如果能读到,就可以泄漏出基址。
具体的方法,构造一个 BigInt64Array 修改 external_pointer ,以及 byte_length ,让 BigInt64Array 能从 js_base 开始访问。
这里由于沙箱,data_ptr 的计算方式改为 js_base + base_pointer + (external_pointer << 2) ,需要注意 external_pointer 变为了偏移,如下图的 0x1000000 。
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 13.png)
修改 external_pointer 和 base_pointer 为 0 ,BigIng64Array 就会从 js_base 开始访问了。
修改全局变量
参考 mdm 提供的 demo https://github.com/mdn/webassembly-examples/blob/master/js-api-examples/global.wat ,添加修改 global 变量的函数。
1 | (module |
用 wat2wasm https://webassembly.github.io/wabt/demo/wat2wasm/ 编译后,提取二进制格式的输出。
现在可以使用 WASM 修改全局变量了:
1 | var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x09,0x02,0x60,0x00,0x01,0x7e,0x60,0x01,0x7e,0x00,0x02,0x0e,0x01,0x02,0x6a,0x73,0x06,0x67,0x6c,0x6f,0x62,0x61,0x6c,0x03,0x7e,0x01,0x03,0x03,0x02,0x00,0x01,0x07,0x19,0x02,0x09,0x67,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x00,0x09,0x73,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x01,0x0a,0x0d,0x02,0x04,0x00,0x23,0x00,0x0b,0x06,0x00,0x20,0x00,0x24,0x00,0x0b,0x00,0x14,0x04,0x6e,0x61,0x6d,0x65,0x02,0x07,0x02,0x00,0x00,0x01,0x01,0x00,0x00,0x07,0x04,0x01,0x00,0x01,0x67]) |
EXP
1 | function dp(x) {} |
[](https://jayl1n.github.io/2022/02/27/v8-sandbox-escape/Untitled 14.png)
参考
- https://mem2019.github.io/jekyll/update/2022/02/06/DiceCTF-Memory-Hole.html
- https://blog.kylebot.net/2022/02/06/DiceCTF-2022-memory-hole/
- https://docs.google.com/document/d/1FM4fQmIhEqPG8uGp5o9A-mnPB5BOeScZYpkHjo0KKA8/edit#
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Global/Global
- https://developer.mozilla.org/zh-CN/docs/WebAssembly/Understanding_the_text_format
- https://github.com/mdn/webassembly-examples/blob/master/js-api-examples/global.wat



