这是用来入门v8的一道很好的CTF题目,主要思路就是利用oob修改v8中JS对象的map,从而造成type confusion
调试环境
具体环境搭建步骤就不详述了,patch文件在这里下载
1 | git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 |
漏洞分析
该题是出题人通过patch,在v8中添加了一个漏洞。所以先从diff文件入手
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
从diff文件中可以看出,出题人为一个Array对象添加了一个oob
函数,具体实现在ArrayOob
。接下来开始分析oob函数。
首先获取了参数的个数len,若个数大于2,则返回undefined
若参数个数为1,则返回下标为length的数组元素,显然这里造成了一个越界读取,访问到了数组后面的一个元素。即read arr[length]
若参数个数为2,则将第二个参数值写入下标为length的元素,造成了越界写入。即 arr[length] = arg
这里需要注意一点,在函数调用时,第一个参数始终为this指针,我们所提供的参数则是从第二个开始的。
漏洞利用
利用技巧
内存布局
因为我们拥有一个越界访问一个元素的能力,那么这里我们需要搞清楚在Array element的后方是什么。
我们首先进行如下调试
1 | d8> var arr = [1.1, 2.2, 3.3] |
从地址很容易可以看出,在Elements的后面紧跟的就是JSArray对象的Map,布局如下图
1 | Elements--->+-------------+ |
这意味着我们可以通过越界访问获取到Array对象的Map地址,同时还可以修改该地址。
trick:在调试过程中会发现,Elements并不是始终紧邻JSArray的,有些时候两者会相距一段距离。在师傅们的wp中提到可以使用splice
来使该布局稳定,例如
1 | var arr = [1.1, 2.2, 3.3].splice(0); |
具体原理我没有查到相关资料。。可能只有等以后读了源码才知道吧
类型混淆
1 | d8> a = [1.1, 2.2, 3.3] |
在v8中,Map描述了一个对象的结构,它决定了如何解析对象中的数据。从以上调试信息中即可以看到a
为Map(PACKED_DOUBLE_ELEMENTS)
,而b
为Map(PACKED_ELEMENTS)
所以,若将一个存放对象的数组的Map修改为一个浮点型数组的Map,再次去读取该数组中的元素时,将会把其中的对象(指针)当做浮点型数据读取出来,这类操作就叫做类型混淆(type confusion)
在此题中,我们可以任意修改和读取JSArray对象的Map,则可以很容易的造成类型混淆。
wasm
在v8利用中总是需要布置shellcode,那么在内存中找到一块具有RWX权限的区域将会十分有帮助。wasm(WebAssembly)详细概念就不在这介绍了,这里值得注意的是是用wasm可以在内存中开辟出一块RWX的内存空间。
这里可以将C语言编写的代码转换为wasm格式。当然,编写的c语言代码不能够调用库函数(不然就可以直接写rce了),但是只要通过漏洞,将我们的shellcode覆盖到内存中wasm代码所在rwx区域即可。
下文将展示如何定位到rwx内存区域
1 | //test.js |
即instance+0x88
处即存放了RWX区域的地址
利用流程(方法一)
- 创建一个数组fake_ab,在element中伪造一个ArrayBuffer
- 创建一个double数组和一个object数组,将fake_ab放入object数组中
- 分别泄露出double map和object map
- 使用double map覆盖object数组的Map,使object数组将其元素当做小数来解析
- 从object中读取出fake_ab的地址,通过固定的偏移(使用splice())计算出elements的地址(即伪造的ArrayBuffer)
- 将elements地址写入objects数组中,并还原objects数组的Map
- 重新将elements取出来,这时将会将其作为一个ArrayBuffer
- 因为elements可以通过fake_ab随意修改,所以我们可以通过控制backingStore来实现任意读写了。
关于伪造ArrayBuffer
在伪造ArrayBuffer的时候需要同时也伪造出它的Map,这部分可以通过调试一个真正的ArrayBuffer并将其Map复制下来(这里并不需要全部的数据)。关于Map的内存模型可以参考这里。
1 | var fake_arraybuffer = [ |
EXP
1 | class Memory{ |
利用流程(方法二)
和前面一个差不多,这里不需要伪造ArrayBuffer和它的map,只需要伪造一个JSArray即可,因为通过修改elements指针也可以实现任意读写。
但是有个问题是,在访问某地址时,需要将elements指针置于该地址-0x10处,因为rwx空间是独立的一块内存页,在其-0x10处的地址是非法的,所以最后还是通过修改ArrayBuffer的backingStore来布置shellcode。
EXP
1 | class Memory{ |