通过上一篇Introduction to SpiderMonkey exploitation.,对JS引擎的漏洞利用有了一个总体的认识。印象中有次比赛碰到了类似的SpiderMonkey题目,于是找到了2018年鹏城杯比赛的hackerscreed这题,同样是SpiderMonkey的引擎,但是从debug symbols来看版本稍微老旧一些,具体的版本号为mozjs-24.2.0。
记得去年鹏城杯线上赛的时候只有一个队伍能够完成这题,赛后准备复盘的时候发现是一道稍作改编的题目,原题来自之前一次韩国的比赛:Javascript Engine(Spider Monkey) Array OOB Analyzing,writeup都有了,只是漏洞函数的名字稍作改变。然而当时对JS解释器数据结构不熟悉,即使有writeup的情况下也没能够弄清楚原理复现,知道通过上一篇所记录的学习过程,才正式把题目做出来。
Vulnerbility
比赛一开始这题就被摆上了台面,也许主办方也意识到了js引擎题目在国内比赛当中的难度还是相当高的,为了调整一下难度,主动放出提示漏洞位于js::array_shift
函数中,其实不需要逆向分析,直接构造测试也能发现问题:
1 | js> a = [1] |
从这里看起来就是shift
函数里面没有对数组length边界做好检查,导致空数组进行shift
操作的时候数组的length被改成-1(0xffffffff)
,继而可以对数组进行越界读写操作(OOB)。根据原题wp所描述的对源代码的修改如下:
1 | js::array_pop(JSContext *cx, unsigned argc, Value *vp) |
可以看到基本就是把index==0
的边界条件给删除了,鹏城杯这题相比原题也仅仅是把pop
函数换成了shift
函数而已。
Head First Exploit
1 | [*] '/Chanllage/PCB2018/hackerscreed/hackerscreed' |
由于保护全开的缘故,原题wp中介绍的是jit引擎的做法,而鹏城杯比赛给的wp中则是写栈上返回值做ROP的方法,我们用传统方法ROP来尝试一下。
由于这题的引擎版本比起上一篇介绍的更为老旧,很多数据结构都发生了改变。我们还是通过Math.atan2
下断点观察对象的方法先看下内存的布局。
1 | b = new Array(1, 2, 3, 4, 5, 6); |
通过js::math_atan2
断点处检查vp[2],也就是这里$rdx+0x10
处的内存,我们找到Array对象的内存,还记得上篇介绍的JSValue
编码方式吗?这里的指针反编码后从0xfffbfffff6644ac0
翻译成0x7ffff6644ac0
。这里也可以看到了数组元素的位置了。上网搜了很久也没能找到gdb中通过指针和结构体symbol打印出对象的办法,算是一个小缺点吧,如果有同学找到相关的办法记得分享一下。在Array这里稍微往下翻还能找到其他的一些地址,比如在b[23]的位置可以泄露出一个elf的地址,由于这题是开了PIE的,所以这条泄露在后面会发挥用处。
继续运行到第二处断点,同样可以找到Uint32Array对象在内存中的位置,下图标出了该对象的size以及其buffer的指针。到了此处任意读读写源语的构造思路就清晰起来了:
- 通过b的越界写改写c的buffer指针和size指针
- 通过对c的读写完成任意读写操作
然而这里的内存分配方式好像和上篇介绍的又有差异,上一篇当中,b和c两个对象在内存中是紧挨在一起的,而这里两个对象却是相隔了0x6b80
字节,因为有ASLR的影响加上对此处的内存管理机制没有深入研究,贸然通过一个硬编码偏移值来通过b的位置计算b的位置显然也是不恰当的,于是这里通过内存搜索的方法来找Uint32Array对象c。
1 | var c_len_idx = 0; |
分析到这里,配合JSValue数据的转换(主要是通过对double型数据的读写)就可以构造出完整的任意读写源语:
1 | b = new Array(1, 2, 3, 4, 5, 6); |
完成了任意读写源语的构建,接下来就是一般linux pwn的漏洞利用思路了,通过泄露必要的地址来bypass ASLR,最后实现在栈的返回地址写ROP的目的,由于ELF是题目全开的,所以需要用以下思路来泄露地址:
- 从
b[23]
中泄露出一个代码段的地址,计算代码段的基地址。 - 通过代码段基地址计算GOT表地址,读取GOT表内容泄露libc地址,计算libc基地址。
- 通过libc基地址计算libc中
environ
变量的地址,读取environ
变量泄露栈地址。 - 观察栈上属于
main
函数的返回地址,用于填写rop chain。 - 最后通过
quit()
触发rop chain。
最后编写rop chain的时候也比较顺利,因为elf体积本身就大,包含了很多gadget,还因为plt段中已经包含了system函数,直接使用即可。
1 | function Int2Array(val) { |
JIT Exploit
根据原题的wp,其实还可以通过改写JIT函数的方法来完成这题,与上一篇新版引擎不同的是,这个版本会把jit编译的字节码直接放到一段可读可写可执行的内存当中(新版不可写)。这样我们可以直接通过把shellcode注入到jit函数的方式来get shell。
原文中是通过搜索特征值的方法来找到jit函数地址的,感觉这种方法还是不太靠谱,而且复现的时候也没有成功。而尝试使用上一篇介绍的寻找指针的方法也没能够找到需要的JSJitInfo
中指向可读可写可执行段的指针。可能还是因为gdb没有根据结构体symbol打印成员变量的功能造成了障碍。感觉这里如果要用JIT的方法来做就只能在to_jit
函数中用const写特征值,然后在内存中搜索特征值定位,随后通过写入大段sled加shellcode的方式来getshell。
看起来非常暴力额,还是做ROP比较可靠吧。