这次跟着SU参加SUSCTF取得了第一名的成绩,希望SU战队越来越好!这道mujs当时自己只看出了有越界写操作,想到了可以进行一个类型混淆的利用,但是最后还是队友做出来了。在此记录一下复线思路(翻译一下队友详细的wp)。
题目描述
出题人给出的题目描述如下
1 | dd0a0972b4428771e6a3887da2210c7c9dd40f9c |
在附件中有mujs
的源码,这个是一个在嵌入式设备上常用的js代码解释器。这个源码的代码量还是很大的。同时附件里还有一个编译好的二进制文件,以及libc文件。从libc文件可以得知远程的运行环境是libc.2.31
题目描述中给出的这个hash字符告诉我们这个源码是来自于这个hash对应的commit的mujs
源码
https://github.com/ccxvii/mujs/commit/dd0a0972b4428771e6a3887da2210c7c9dd40f9c
所以使用diff对比了这两个源码。发现主要的差别在两个地方
- 一些内置方法在main.c中被禁用了
- 新增了dataview.c文件。这个是[DataView](DataView - JavaScript | MDN (mozilla.org))方法的一个简化版的实现
寻找漏洞点
队友的思路首先是从最近的CVE里寻找一些漏洞,但是没有发现有用的信息,所以这个题应该是魔改的这个版本的源码。而且被魔改的部分其实代码量不算大,直接审就好了。
首先我们需要理解DataView都做了什么,都有哪些方法。一些常用的用法如下所示。
1 | x = new DataView(10) |
其实从jsB_initdataview
函数当中大概可以看出来都有哪些方法,然后自己试一下就可以试出来这些方法怎么用
1 | void jsB_initdataview(js_State *J) |
然后经过一阵审计,很容易就能发现这里存在一个越界写操作,可以越界写9字节
1 | static void Dv_setUint8(js_State *J) |
值得注意的是这里同时也存在一个整数溢出(但是是无符号的),可以让我们可以前溢9字节。但是由于这里没有什么free的操作,所以很难利用。因此还是后溢9字节可用性高一点。
利用漏洞
类型混淆
因为说溢出9字节,这个多出的一字节很容易令人联想到类型混淆。下面是 js_Object 的结构。可见只要溢出一字节就可以覆盖它的type字段。
1 | struct js_Object |
下面给出类型混淆的poc
1 | b = DataView(0x68); |
输出为
1 | [object DataView] |
越界写Dataview的Length字段
js_Objec 使用了 C语言里的union结构,所以不同类型可以共用相同的内存。队友的想法是利用与DataView里Length字段占用内存相同的其他类型的字符来修改DataLength。这样我们就可以扩大任意地址读写的范围,起码可以拓展到整个堆上了,而不仅仅是越界9字节。
整个Js_Objec 结构体如下:
1 | struct js_Object |
比如js_Object.u.dataview.length
在结构体内所处的偏移是和js_Object.u.number
以及s_Object.u.c.name
这两个是相同的。
所以我们可以修改js_Object.u.number
,队友找到了下面的代码
1 | static void js_setdate(js_State *J, int idx, double t) |
让我们试一下
JS_CDATE
的值是10,我们需要把这个DataView结构的type字段溢出成10就可以了
1 | b = DataView(0x68); |
结果:
1 | [object DataView] |
Emmm,居然是报错了。难道进行了类型混淆还是不能调用setTime方法么?队友曾经为了这个问题困扰了许久,他意识到了对象的prototype 在我们一创建的时候其实就已经确定了。所以当我们改变type的时候prototype并没有改变。而prototype基本就已经定义了这个对象可以调用哪些方法,可恶。
这时无敌的队友发现,js里有个讨厌的东西叫 this
,这个东西在这个时候算是雪中送碳吧
我们仍然可以通过js的bind
调用setTime
:
1 | Date.prototype.setTime.bind(c)(12) |
成功了!
1 | b = DataView(0x68); |
看到这里大家可能会有些疑问,就是u.number
是8字节的double
类型,而我们要覆盖的u.dataview.length
只有四字节,这样会不会覆盖到后面紧跟着的四字节的u.dataview.data
,毕竟这个是个指针,覆盖掉了容易导致crash。其实是不会的,因为这个结构体有8字节对齐。
使用堆上的越界读写来实现代码执行
到了这个阶段,我们已经可以通过修改dataview的length字段来实现堆上的任意地址读写了。并且堆布局也是我们相对可控的了。为了更好的控制堆上的结构,我的队友在c
后面又申请了两个Dataview。并且我们知道,如果我们申请的堆的大小大于128k的话我们会使用mmap来申请空间,这个是malloc函数的一个策略。而这个mapp的地址往往距离libc地址很近,因此我们可以通过这种方法来泄漏libc基地址。
所以我们用上述的方法泄漏了libc地址之后,可以伪造一个JS_CCFUNCTION
类型,他有一个字段叫做u.c.function
我们可以轻易用下面的方式调用这个函数指针
1 | void js_call(js_State *J, int n) |
最终exp
1 | b = DataView(0x68); |
队友表示他之前也没做过这种mujs的利用,但是这些堆利用的基本思路和很多大型项目比如v8的利用是共通的,但是那些大型项目由于运行时更为复杂,堆空间要相对更不可控一些。