一、What is Sandbox
二、V8 Sandbox
三、V8 Sandbox Breaking
四、Conclusion
沙箱机制(Sandboxing)是一种安全技术,用来隔离运行中的应用程序或代码,使其在受限的环境中执行。这种机制的目的是限制应用程序或代码与系统资源(如文件系统、网络、硬件)的直接交互,从而防止恶意软件或不受信任的代码造成安全威胁或数据泄露。
当提到chrome沙箱时通常会想到的是用于限制应用程序或代码与系统资源的直接交互的沙箱,而在实际的漏洞利用层面内存安全仍然是一个重要问题,在众多chrome可利用的漏洞中,V8漏洞可以说占到大多数,而V8漏洞很少是“经典”的内存损坏错误(释放后使用、越界访问等),而是微妙的逻辑问题,反过来再利用这些问题来损坏内存。因此,现有的内存安全解决方案在很大程度上并不适用于V8,于是在此背景下衍生出了V8 Sandbox。本文主要对V8 sandbox的一些绕过方法进行汇总分析。
V8沙箱是一个基于软件的沙箱,其背后的基本思想是隔离 V8堆内存,使任何内存损坏都不会“扩散”到进程内存的其他部分,从而增加V8漏洞的利用难度。具体实现方式有两种:第一种,如果buffer位于沙盒内,就将40位的地址偏移左移24位后得到的64位结果写入相应字段地址中:
-
disable sandbox:
-
enable sandbox:
通过对%DebugPrint具体的实现代码下断,可以找到具体的decode过程,首先从指定字段地址出得到64位值(sandboxed_pointer
),再将sandboxed_pointer
右移24位(kSandboxedPointerShift
)得到偏移(offset
),最后将offset
与基址(cage_base
)相加得到真实的地址指针:
第二种,如果buffer位于沙盒外,则会将指定字段地址内的值作为索引,通过指针表间接的引用buffer:
例如blink对象,在V8中所有的blink对象都分配在V8堆外,并以api对象的形式在V8中表示:
V8 api对象实际上是blink对象的包装器,其中embedder fields
字段存储内容用实际为一个表索引,此索引位置保存着对应的blink对象的实际地址及其类型:
同样也可以通过对%DebugPrint实现下断找到具体的decode过程:
signature confusion breaking sandbox
在V8 webassembly中,wasm模块导出函数主要由函数签名(signature)和函数实现(call_target)组成,假设有以下代码,此代码可以导出read_0
与read_1
两个函数:
在js代码中使用read_0(0x41)
触发对read_0
函数的调用,然后对Builtins_JSToWasmWrapper
下断可以得到signature
与call_target
的获取过程:
1. 先通过函数对象获取shared_info
字段,其中r14寄存器存的一直都是基地址,而rdi则是函数对象地址:
2. 通过shared_info
字段得到function_data
:
3. 通过function_data
获取signature
,signature
对象不在沙盒中,所以是通过外部表的形式间接引用的,所以此处得到的是一个表索引:
4. 通过function_data
获取func_ref
:
5. 通过func_ref
可以得到internal
,internal
是一个外部对象,而call_target
就在internal
中并且也是一个外部对象,所以都只能得到一个表索引:
最后Builtins_JSToWasmWrapperAsm
函数会通过call rdx
进入call_target
指向的地址,在通过几个Jmp后会进入真实的jited代码,rax为传入的地址偏移:
总结整个获取过程大致就是:function -> shared_info -> function_data -> func_ref -> internal -> call_target
通过调试会发现signature
与call_target
并没有太多的联系,而wasm导出函数的参数类型,及其后面的返回值类型声明列表由signature来决定,而对类型的检查也是在builtins函数中,所以在调用call_target
时会直接将参数传入:
所以如果将read_0
与read_1
的call_target
进行混淆那在调用read_1
函数时就可以实现64位地址空间的读取,在前面的分析中可知call_target
是沙盒外对象,所以只能得到一个表索引无法直接读取到call_target
对象地址,不过func_ref
在沙盒内,可以直接将read_0
的func_ref
写入read_1
。还有一个问题,那就是read_0
的call_target
代码在从内存中读取内容时还会与rcx也就是(memory 1)
代码中申请的线性内存地址的基址相加,这个线性内存地址实际为一个arraybuffer
对象的backing_store
:
通过前面第一章对JSArrayBuffer backing_store
对象的说明,可以提前得到rcx中的值,当然得到的是其偏移地址,而基地址可以用Uint32Array
对象来泄露,用泄露出的基地址加偏移地址就可以得到真实的backing_store
地址,现在用我们要读写的64位地址减去backing_store
地址,再将结果传入对应的读写函数就可以实现64位地址空间读写。
为方便解释,以下所有代码示例将通过sandbox对象来实现双数组混淆来实现读写原语的构造,此对象主要用对沙箱的测试,在稳定版中不可用,官方说明:V8 Sandbox - Readme (googlesource.com)。
以下wasm代码与开头的类似,为了方便构造写原语我又添加了oob_write
与do_write
函数:
之后将转换为二进制数的wasm代码放入数组中,以便于将函数导入到js代码中调用:
然后利用sandbox对象来完成双数组混淆,具体做法就是直接去修改数组d_arr
的长度字段,将其长度修改为0x1000,此时d_arr
就可以去读写o_arr
数组中保存的数据内容:
在得到两个混淆的不同类型的数组后就可以像其他V8漏洞那样构造地址泄露函数:
此时我们就可以着手准备构造任意地址读写原语了,首先通过混淆数组得到double型数组的map,随后在一个新的double型数组f_arr
中伪造一个假的double数组fake_obj
,之后我们就可以通过f_arr
去控制fake_obj
的elements地址,但要注意的是因为堆沙箱的存在,被填入的elements地址将被限制在堆沙箱内存区域内:
在得到读写原语与地址泄露函数后就可以通过上文中提到的方法去混淆call_target
了:
最后要注意的是,oob_write
函数在向内存中写入数据时还会将传入的目标地址与wmemory堆地址相加,所以为了确保写入的地址正确,我们还需要得到wmemory堆地址,并用要写入的目标地址减去wmemory堆地址,这样在写入时就可以写入到正确的目标地址:
此方法在chrome 126版本之后修复,在修复程序中添加了新的IsAccessedMemoryCovered
函数:
此函数先回检查目标地址是否为空,如果不是将会检查访问的目标地址是否位于沙盒内。gV8SandboxBase
与gV8SandboxSize
也是新添加的内容,gV8SandboxBase
为沙盒区域的基地址,gV8SandboxSize
则是沙盒区域的大小。
blink object confusion breaking sandbox
一开始提到在V8中所有的blink对象都以外部指针表索引的形式被V8 api对象所引用,虽然外部指针表受到保护无法篡改里面的内容,但是api对象是在堆中,可以篡改api对象中的embedder fields
字段,使两个blink对象产生混淆,比如将DOMRect
与 DOMTypedArray
混淆。
DOMRect
对象只有四个属性:x
、y
、width
、height
,访问这些属性本质上只是对相应对象的指定偏移进行读写,如果DOMRect
与 DOMTypedArray
发生了混淆,那就可以通过DOMRect
中的属性字段自由控制DOMTypedArray
对象的backing_store
指针,该指针用于指向DOMTypedArray
实际的数据存储区域,可以将此指针覆盖修改为其他任意64位地址指针从而实现64位地址空间读写。先创建要被混淆的DOMRect
与 DOMTypedArray
:
此处与上一种方法相同,同样使用sandbox构造堆读写原语,不再进行说明:
最后将DOMRect
与 DOMTypedArray
进行混淆:
此方法至少在chrome 126版本之前都可用。
尽管 V8 引擎在其设计中引入了沙箱机制以增强安全性,但攻击者仍然能够通过复杂的对象混淆和内存操控来打破沙箱的边界。因此,在未来的安全研究中,如何更好地隔离这些敏感数据对象以及如何进一步优化沙箱设计将成为研究重点。
4. Sandbox
【版权说明】
本作品著作权归Anansi所有
未经作者同意,不得转载
天工实验室安全研究员
研究领域:Chrome浏览器漏洞研究
原文始发于微信公众号(奇安信天工实验室):V8堆沙箱绕过方法分析总结
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论