OOB 复现
1 | git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 -f |
在编译前,需要先在 out.gn/x64.debug/args.gn
中加入以下代码:
1 | v8_enable_backtrace = true |
V8 常用调试命令如下:
1 | set args --allow-natives-syntax --shell ./exp.js |
漏洞分析
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
- 为 JSArray 对象新增了一个 ArrayOob 方法:
1 | BUILTIN(ArrayOob){ |
- 如果输入参数个数为“1”:把下标为
length
的元素读取出来(越界读1字) - 如果输入参数个数为“2”:在下标为
length
的元素处写入value
(越界写1字) - 否则返回
undefined_value
- PS:
this
也算一个参数
JS 对象内存信息布局
现在有 “越界读1字节/越界写1字节”,要想成功利用则需要先掌握 JSArray 对象的内存布局
- 测试用 JavaScript 代码如下:
1 | function Foo(properties, elements) { |
- 使用 GDB 进行打印:
1 | d8> %DebugPrint(foo) |
- 打印 JS_OBJECT_TYPE:
1 | pwndbg> x/20xg 0xebe3f60df81-1 /* JS_OBJECT_TYPE */ |
- 前3个数据分别为
map properties elements
的地址 - 接下来的10个一字空间分别用于存储 [
property0
:property9
] - PS:没有使用指针压缩技术(直接把8字节的指针存入内存)
V8 拥有两种类似的属性:
- 索引属性(Array-indexed Properties)
- 命名属性(Named Properties)
V8 遍历时一般会先遍历前者,前后两者在底层存储在两个单独的数据结构中,分别用 elements
和 properties
两个指针指向它们
- 如果命名属性少于等于10个时,命名属性会直接存储到对象本身,而无需先通过 properties 指针查询(直接存储到对象本身的属性被称为对象内属性 In-object Properties)
1 | pwndbg> x/20xg 0x00000ebe3f60eb41-1 /* properties */ |
- 从第3个指针开始,就是:[
property10
:property11
](前10个存储在对象内部)
1 | pwndbg> x/20xg 0x00000ebe3f60e019-1 /* elements */ |
- 从第3个指针开始,就是:[
element0
:element11
]
对于本题目来说,更重要的特性是:
- JSArray 对象的
elements
就分配在 JSArray 的相邻上方
测试案例:
1 | var floatArray = [1.11, 2.22, 3.33]; |
1 | d8> %DebugPrint(floatArray) |
- JSArray 所在地址为
0x116973d4e061-1
1 | pwndbg> job 0x116973d4e061 /* JSArray */ |
1 | pwndbg> job 0x116973d4e039 /* JSArray->elements */ |
- JSArray->elements 所在地址为
0x116973d4e039-1
1 | pwndbg> p {double}(0x116973d4e048) |
- 可以发现:JSArray->elements 和 JSArray 是相邻的
入侵思路
我们拥有一字的越界写和一字的越界写
这一字的越界读刚好可以泄露出 JSArray->map:
1 | var floatArray = [1.11, 2.22, 3.33]; |
而一字的越界写则可以对 JSArray->map 进行覆盖,既然可以操作 JSArray->map,那么最优的利用方式肯定就是类型混淆:
- 由于 JSArray 是利用
map
来判断一个elements
到底是数字还是指针 - 如果我们可以修改
map
,就可以触发干扰 V8 对类型的判断
常见的类型混淆如下:
- 指针 -> Double:数字类型可以直接获取数值(用于泄露某个 JS 对象的地址)
- Double -> 指针:指针类型会被当做一个对象(用于伪造一个 JS 对象,用于 WAA/RAA)
具体代码细节如下:
1 | var obj = {yhellow:"yhellow"}; |
接下来就要实现 WAA 和 RAA,可以通过将 JSArray->elements 数组伪造成一个数组对象,实现任意地址读写
- 在
elements[0]
写入一个数组的map
,在elements[2]
写入target - 0x10
- 然后再用
fakeObject
方法将elements[0]
的地址伪造成一个对象fake_array
- 再对
fake_array[0]
进行读写,实际上就是对目标地址进行读写了
模型如下:
详细代码如下:
1 | var floatArrayAddr = addressOf(floatArray); |
- 任意写 WAA 这里需要注意一下,由于 V8 的保护,不能直接将数据写入
free_hook
,需要Dataview
作为中介
接着需要完成 libc_base
的泄露:
1 | 60f:3078│ 0x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push rbp |
- 在
objAddr - 0x8000
后面有一些特殊的指令push rbp
- 可以利用这里泄露出
pro_base
- 然后就可以利用
pro_base
计算出 GOT 表地址,并泄露出libc_base
1 | while(1) |
这种泄露并不稳定,有许多偏移都需要手动计算,下面是一种稳定的泄露:
- 打印
floatArray->ArrayConstructor
属性:
1 | d8> %DebugPrint(floatArray.constructor); |
1 | pwndbg> job 0x2be6ab0d0ec1 |
- 打印
floatArray->ArrayConstructor->code
属性:
1 | pwndbg> job 0x01c365606981 |
- 可以通过这里来泄露
pro_base
- 然后利用同样的方法来泄露
libc_base
1 | var pro_base = leak_addr - 0xfc8780n; |
最后在 free_hook
中写入 system
就可以了
另外,还有一种不需要进行泄露,直接注入 shellcode 的方法:
- 通过 WASM,能得到一块 RWX 的内存,里面放着WASM的二进制代码
- 将 shellcode 写入到这块内存,再调用 WASM 接口时,就会执行 shellcode 了
1 | var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1, |
- 这是一个比较套路的过程
完整 exp:
- 不稳定泄漏:
1 | var obj = {yhellow:"yhellow"}; |
- 稳定泄漏:
1 | var obj = {yhellow:"yhellow"}; |
- 利用 WASM 执行 shellcode:
1 | var obj = {yhellow:"yhellow"}; |
小结:
巩固一下 V8 pwn 的利用手段
根据最近复现的这两个 V8 pwn 和之前遇到过的 JavaScript pwn,总结一下这种 JavaScript 引擎题目的解决思路:
- 先分析 patch,了解大概的漏洞点
- 然后想方设法实现
addressOf
RAA
WAA
这3个函数 - 最后尝试泄露或者注入 shellcode
如果可以接触到 JS 对象的 map
,那就要考虑使用类型混淆