[toc]
这是Pwn2Own黑客大赛上用于攻破IE9的漏洞
分析环境
Windows 7 32位
VM 12
IE8
windbg
ida
基于HPA的漏洞分析方法
poc
1 | <html> |
首先开启页堆
1 | C:\Program Files\Debugging Tools for Windows (x86)>gflags.exe -i iexplore.exe +hpa |
打开poc,附加,允许阻止内容,我一下子就断下来了,不用设置子进程调试模式
1 | 0:013> g |
可能作者附加的进程不是主进程,试了一下,果然是这个问题。
看下栈
1 | 0:005> kb |
向上看,edi来源与esi,esi这个函数看不到,是上层函数,也就是mshtml!CTableLayout::CalculateMinMax
1 | mshtml!CTableColCalc::AdjustForCol: |
我们在mshtml!CTableLayout::CalculateMinMax下个断点跟一下
1 | 0:013> bp mshtml!CTableLayout::CalculateMinMax |
我们看到传进该函数的第一个参数是CTableLayout对象this指针(因为第一个地址是一个虚表),
单步到下面这行,这就是poc里面的span的值1了
1 | 0:005> p |
我们用ida看看器传递路径,给到了arg0
1 | .text:74D301A6 mov eax, [ebx+54h] |
后面赋值给了edx
1 | 0:005> p |
跟着ebx+94h右移两位后会和我们的edx比较,当然0右移多少还是0,所以是跟0比较
1 | 0:005> p |
之后下面这个函数里面是分配内存的
1 | .text:74D302EC push 1Ch ; unsigned int |
跟进去,发现是分配0x1c*4的内存
1 | 0:005> p |
实际里面调用的是_HeapRealloc
1 | 0:005> p |
那我们继续跟,跟进GetAAspan,返回值为000003e8,即10进制的1000,也即trigger函数里面将span设置成了1000
1 | 0:005> p |
获取width的返回值有点怪
1 | 0:005> p |
但是一算跟我们的42765有点关联,就是42765*100
1 | 0x00414114 |
继续,由于这次调试ecx为0,所以还没触发异常,之后还会将eax给了[ebp-24h],这个之后会给esi
1 | 0:005> p |
可以看到确实是的
1 | 67545b83 8b75dc mov esi,dword ptr [ebp-24h] |
第一次传入的esi是没什么问题的,看到+0x18也是没问题的
从调试结果看esi每次递增0x1c,而且第一次的时候不会递增
ida看到,这确实好似每次都会递增0x1c,即10进制的28
而控制循环次数的就是在这一句
1 | if ( (signed int)v154 >= v161 ) |
因为v154在循环外初始化为0,所以核心应该是v161
我们向上寻找161的来源,是CTableCell::GetAAcolSpan或者上面那个,看名字就猜到是获取clo的span值的,而且最大是1000,我们看看调试是哪个,
调试发现是调用GetAAspan(第一处),返回值为1,跟poc符合
1 | 0:013> g |
第二次就返回0x38e,即1000,就是poc中将span修改为1000
1 | 0:005> g |
而这个堆块本来分配的时候就只有0x70,我们也可以看到是在 66e78ffb mshtml!CImplAry::EnsureSizeWorker+0x000000a1分配的
1 | 0:005> g |
所以整个漏洞其实本来span为1,堆分配了0x70的大小是足够的,但是当执行时间触发器的over_trigger函数时候,将span改写为1000,导致循环次数增大,最终导致越界写到还没分配的地址(因为大小只有0x70啊),触发异常
那么从这里看width暂时没起作用,可能是漏洞利用的时候会用得,将width的赋值删掉也是能触发漏洞的
信息泄露原理分析
首先是先布局,复制一下代码
1 | <div id="test"></div> |
首先是搞了3个480长度的字符串,之后声明了两个数组,跟着实际复制0x100大小的复制到数组里面,arr数组其中A和B是连续的,而rra中E是间隔的,之后还新建一个button对象
最后还有个循环将rra置空,,之后回收垃圾,这个相当于free吧
之后便是一堆table,col元素
为了弄清整个过程我们需要看看CollectGarbage回收的内存会不会是我们之后触发漏洞申请的内存,是的话就好办了
我们调试看看,字jscript加载的时候下断点
1 | 0:013> sxe ld:jscript |
之后对JsCollectGarbage下断点(其实释放内存的函数应该都会调用底层函数ntdll!RtlFreeHeap)
1 | 0:005> bp jscript!JsCollectGarbage |
根据作者的思路,我们先对RtlFreeHeap下断,从他的定义可以看到第3个参数就是释放的地址(可以从msdn看到)
1 | BOOLEAN RtlFreeHeap( |
先禁用我们的JsCollectGarbage,对RtlFreeHeap下个记录断点(注:echo后面的其实是输出字符串:free heap)
1 | 0:005> bl |
同时我们也关注漏洞位置分配的内存,我们在下面这行下断点(作者输出的是ebx+0x9c的值,这是之前调试的时候发现的值)
1 | 0:005> bu CTableLayout::CalculateMinMax+0x16d ".echo vulheap;dd poi(ebx+9c) l4;g" |
由于这个输出太多,我们输出到文件
1 | 0:005> .logopen c:\log.txt |
我们查找一下vulheap字符串
vulheap
03f35e68 0000054b 00450045 00450045 00450045
我们看看我们之前释放的内存有没有03f35e68,查找一下果然有
其实还有很多个这样的,因为毕竟是133个col元素
此外在poc中,将span设置为19,
1 | var obj_col = document.getElementById("132"); |
这里作者说得不是很清楚,首先id为132的col元素的span为9
那么一开始分配的内存是9*0x1c=0xfc
,这个放到0x100已经释放的内存应该是挺合适的,内存管理器就是要找到合适的内存,道理应该是这样的
我们看看vulheap到底长怎么样(下面是信息泄露完后的情形),需要重点注意的是前面还有后面的一些地方被被覆盖为04 10 00 00 即0x00001004,这个正是width*100
的值
而程序的异常复制中复制的源地址是是CTreeNode::GetFancyFormat
返回地址指向的值,一次复制一个dword,所以我们看到vulheap还有EE..的残留,而且复制的目标是0x18偏移,每次还再偏移0x1c
我们看看CTreeNode::GetFancyFormat
的某次返回值,之后还要+0x70
1 | .text:74EC5A4A call ?GetFancyFormat@CTreeNode@@QAEPBVCFancyFormat@@XZ ; 某次返回值:0x02a70a20 |
我们看一下复制源,是00010049
1 | 0:005> dd 0x02a70a20+70 l1 |
那么上面的vulheap应该是00010049而不是1004啊,为什么呢
我下个断点看看
1 | bp mshtml!CTableLayout::CalculateMinMax+0x16d ".echo vulheap;dd poi(ebx+9c) l4;ba w4 poi(ebx+9c);g" |
也是断在AdjustForCol函数,而ebx正是0x1004
1 | 0:005> g |
我们看看上一个地址是什么呢,原来是这两条语句
即下面这个位置啊,这样调试过后就很清楚了
之后我们看看over_trigger函数
1 | function over_trigger() { |
我们先来实践一下javascript对字符串的访问,可以看到,你的字符串有四个,想访问后面的地址是不可能的,返回是空字符串
根据书中信息,加上自己的调试,第一个字节表示字符串长度0xfa,而0xfa正是0x100-6
那么假如我们改变了长度的值,那么我们就可以任意向后读了,那么怎么覆盖呢,就是利用前面的span值增大了,导致循环次数增加,内存每次递增0x1c去覆盖,那么假如我们能够精准覆盖头指针那就可以了
那这里下断点就很纠结了,没灵感的时候怎么想都不行,有灵感就很好了,其实调试过程中下断点太重要了
我下的断点如下,在执行将span赋值为19之前下断点
1 | bp mshtml!CTableColCalc::AdjustForCol+0x2a ".if(@ebx ==0x1004){.echo ------------------;db esi}.else{gc}" |
其实暂停了好几次,下面是最后一次,我们看到BBBBB的那个字符串的前面的四个字节被改写为48 00 01 00了,这我们就有点奇怪了,为什么不是04 10 00 00
1 | 0:005> g |
那我们在上面那个断点的基础上再下一个写入断点(由于多次调试位置会变化)
首先第一次断下来,我们发现正是我们之前预测的这个10049的写入,那么上面的结果为什么比这个少了1呢
1 | 0:005> g |
我们再g一下
1 | 0:005> g |
我们看到eax正好是00010048,原来是在SetValue里面改变了,那整个过程就很清楚了,那么实际利用的是下面这里,而且a1是width相关的
我们手动运算看看,验证完成
还有其实作者的运算并不对,应该是下面的
1 | >>> hex(0x1c*18 + 0x18) |
0x210是刚好,vulheap是0x100,”AAAA”是8 + 0x100 = 0x108,而”BBBB”的头部是8,所以再后面的就是BBBB的长度值,刚好位于0x210
而且i最大为18,不会19(可以看到下面的地方是等于,v154是+1之后再判断的)
那么读取的时候是怎么样的呢
1 | var leak = arr[i].substring((0x100-6)/2+(2+8)/2, (0x100-6)/2+(2+8+4)/2); |
其实简化一下就是
1 | var leak = arr[i].substring((0x100-4)/2+8/2, (0x100-4)/2+(8+4)/2); |
0x100-4是减去4字节的头部长度值,后面的8是CButton的堆头指针,如下图(下面为十六进制值)
我看看计算得对不对,因为偏移量可能不一样
可以看到是不对的
1 | 0:007> lmm mshtml |
我们重新算一下偏移,这样才对,跟作者的也一样了
1 | 0:007> ? 66ed3af8 - 66d60000 |
漏洞利用
首先我们将偏移改回来
我们先看触发漏洞,再看堆喷射
1 | function smash_vtable(){ |
将width设置成这么大,而且后面有个注释是vftable,应该是覆盖vftable,之后再想办法调用被覆盖的虚表里的函数
先计算一下,确实如上面注释所写的那样
1 | >>> hex(1178993*100) |
但是作者给的exp已知断不到点子上,异常经常不一样,所以我改用msf的exp
奇怪的是msf竟然没有信息泄露代码,先直接运行
1 | 0:005> g |
我们看看返回地址,原来调用了这里,跟作者的一样
那我们在mshtml!NotifyElement+0x35(上面的kv显示的0x41是错的)下断点发现这里是不断被调用的,根据layout这个东西,应该是实时地去调整浏览器布局,一有“风吹草动”就调整布局,比如我们通过javascript删除了某个元素,布局也要调整
1 | 0:013> bp mshtml!NotifyElement+0x35 |
我们看下eax的来源,是来源于ecx,而ecx指向的地址正是被覆盖成了0707002c
经过调试,发现这个虚表是在CButton的第3个dword的位置
那就是通过下面这个覆盖的
那么覆盖大小这么算呢
heap+A+B+0xC就是0x100+0x108+0x108+0xc = 0x21c,因为A,B字符串有堆头的8个字节
其实之前的span为19足够覆盖的了
1 | >>> hex(0x1c*19) |
覆盖+8的位置就是,刚好
1 | 0x214+8 = 0x21c |
那整个过程就清楚了,但好像19触发不了,应该值没变不会进入那个流程,应该44确保可以触发那个流程吧
我对msf的代码添加了信息泄露,同时修改了rop(这个重新自己用mona生成后根据泄露的基址调整的)
1 | <html> |
结果
漏洞修复
就是在span更改后重新分配相应大小的内存即可
总结
- 漏洞的原因是span值改变后内存没有重新分配或者再次申请更多内存,导致可以越界写
- 在信息泄露的时候通过巧妙的构造,覆盖字符串头部的长度值,可以泄露出CButton虚表指针,从而通过偏移计算存储mshtml的基址
- 在漏洞利用的时候,通过特定的长度值,覆盖虚表指针,导致任意代码执行,通过堆的巧妙布局,精确地控制数据的布局,利用rop绕过DEP,成功执行代码
漏洞之外的东西
- 调试过程中下的断点太重要了,只要下对了,一下就找到关键点,整个人都阔然开朗
- 调试ie的漏洞,特别是javascript相关的,用javascript断点或者alert可以大大节省调试时间,因为你下一个断点可能断得太多,你让javascript执行到那里采取启用断点,那就可以节省很多时间了