本篇参考以下文章:
[1]http://www.geoffchappell.com/viewer.htm?doc=notes/security/aurora/index.htm
[2]http://securitylabs.websense.com/content/Blogs/3530.aspx
[3]http://www.securityfocus.com/archive/1/508961
其中,[1]对此漏洞进行了极其详细的解说(就差告诉你怎么调试了),我就是看了它才了解细节上是怎么一回事,[2]则简要地进行了解说,而[3]则直接说出了调试的提示。
因此,我这里只是看了以上参考文章之后,依葫芦画瓢调试了一下,把关于调试和理解的部分整理了一下,观点并不是我自己首先得出来的。
调试环境:XP sp3 简体中文版系统,IE7(IE6更容易跳进shellcode,IE7跳进shellcode概率似乎不大,不过这里主要是为了讨论漏洞怎么来的,IE7虽跳不进shellcode,但是也会因为访问无效地址而抛出异常,照样可以断下,所以并不影响下面讨论)。
先观察PoC网页,这里的引用把heap spray等略掉了,完整的在附件:
简单地理解,Body中有个span对象,span对象中包含一个img对象,img对象的onload响应例程中,调用createEventObject创建了相应event对象的一个副本,然后将span对象的innerHtml清空,这样导致img对象的释放。在此之后,创建一个定时器,在超时例程中访问新event对象的srcElement属性,应该是在这时候触发了漏洞。
winbg载入IE7(PoC文件名为命令行参数),IE7出现网页脚本或ActiveX对象被阻止提示,确认允许运行,等许久后抛出异常断下
从栈回溯信息看,这的确是在定时器例程中使用event_obj.srcElement时出现的异常。
触发异常的点很容易找,但是找到为什么却不容易。
关键是要了解CEventObj::get_srcElement怎么实现的,为什么这么实现,这是我看了[1]之后才了解的。简单地说,为在event中能够访问相应的Element,CEventObj并不是直接就在其类中保存一个CElement结构的指针,而是绕了几绕:CImgElement对象创建后,又创建了相应的CTreeNode对象,由CTreeNode对象的属性中保存CImgElement类指针。然后将CTreeNode对象的地址,保存在这个img的事件对象CEventObj类的一个EVENTPARAM结构中。
对此,参考文献[3]直接给出了调试的提示:
重新载入,在系统断点时输入命令bu mshtml!CImgElement::CImgElement,g运行,停在断点处一次,这时再次g运行,IE7出现网页脚本或ActiveX对象被阻止提示,确认允许运行,等许久让它Heap spray,之后又断在CImgElement::CImgElement
这时得到将创建的CImgElement对象指针,ecx=ecx=002033d0
bu CTreeNode::CTreeNode,运行断下
可以看到它是由CHtmRootParseCtx::BeginElement调用的,ecx=02b60880为CTreeNode类指针,该函数第二个参数002033d0正是刚刚创建的CImgElement类指针,函数调用CTreeNode::SetElement将该CImgElement类与CTreeNode关联:
步过以上部分,在此之后,网页代码使用createEventObject创建了event的副本,很的是,这个新的CEventObj类,当然copy了原来对象的EVENTPARAM结构,但是根据[1]的解释,在EVENTPARAM结构内容被copy的时候,并没有增加CTreeNode的访问计数!
CEvent::Create中调用EVENTPARAM::EVENTPARAM复制EVENTPARAM结构,但后者的代码中的确没有包括操作其中CTreeNode(在后面的CEventObj::get_srcElement函数分析中可以看出,CTreeNode类指针在EVENTPARAM结构的头部)增加访问计数的内容。
于是通过调用document.getElementById("sp1").innerHTML = "",导致CTreeNode和CImgElement的释放,看看CTreeNode开头处的CImgElement类指针是什么时候被改掉的,对CTreeNode类头部下硬件写入断点:
看来随着这个过程中CTreeNode::Release的确被调用,该地址的内容也就被修改了,不再指向有效的CElement类(或其子类)对象。
接下来就是CEventObj::get_srcElement了,先IDA分析一下,当CEventObj::get_srcElement被调用时,进入CEventObj::GenericGetElement:
1. 调用CEventObj::GetParam得到EVENTPARAM结构指针
2. 从EVENTPARAM结构中得到相应CTreeNode指针:
3. 从CTreeNode指针中直接得到CElement对象(这里对于原event来说,是其派生类CImgElement对象)指针,并调用CElement::Doc
4.CElement::Doc函数调用CElement::SecurityContext:
在漏洞触发时,EVENTPARAM中带的CTreeNode类的内容已经不对了,从而从其中的CElement类地址也是不对的,这时把它作为CElement类指针调用CElement::Doc便会出错,导致抛出异常:
验证一下是不是原来的CTreeNode对象的指针位置:
可以看到[ebp-8]的EVENTPARAM结构体中的确还保留着指向原CTreeNode的地址值02b60880,而此时由于该处被修改了,当前值00060006为一个无效指针,当程序把它当成CElement类指针调用CElement::Doc时,就出现了访问无效内存的Access Violation异常。
这是触发不能进入shellcode的情况,如何进入shellcode呢?PoC中定时器超时例程前面的内容:
可以看到,网页代码在通过改变span对象的innerHtml造成CTreeNode对象的释放后,企图重用CTreeNode对象原来占有的内存空间,将其用0x0a0a填充。
可以推想,如果这个动作成功,那么原CTreeNode对象开头位置会被0x0a0a0a0a所覆盖,则:
1. CEventObj::GenericGetElement中取到的“CElement对象地址”也就会变成0x0a0a0a0a
2. 以此地址来调用CElement::Doc,将会访问dword ptr [0x0a0a0a0a]取虚函数表
3. 而由于之前的heap spray,0x0a0a0a0a处的DWORD值同样为0x0a0a0a0a,这样0x0a0a0a0a又被当成了虚函数表地址
4. dword ptr [0a0a0a0a+34]同样是0x0a0a0a0a,这样0x0a0a0a0a又被当成了虚函数的入口址
5. 以上导致代码变为call 0x0a0a0a0a,而同样地由于heap spray,在等效于nop的0x0a指令后面跟随的,就是shellcode代码,于是程序进入了shellcode代码。
小结:
1. 为在event中能够访问相应的Element,CEventObj并不是直接就在其类中保存一个CElement结构的指针,而是在CElement对象创建后,又创建了相应的CTreeNode对象,由CTreeNode对象的属性中保存CElement类指针,并将CTreeNode对象的地址,保存在CEventObj类的一个EVENTPARAM结构中。当网页代码中调用event.srcElement时,CEventObj::get_srcElement函数通过EVENTPARAM结构中保存的CTreeNode对象及其中保存的CElement对象指针来访问CElement对象。
2. 在使用createEventObject(event)创建event的副本的时候,EVENTPARAM结构同样被复制,然而EVENTPARAM中保存的CTreeNode对象的引用计数没有增加。
3. 第2点导致一旦在Element的event响应例程中调用createEventObject(event)之后取消相应Element(如PoC中将img所在的span的innerHtml清空),则相应的CTreeNode对象也会被释放,从而新event中的EVENTPARAM结构指向的CTreeNode指针不再有效而会被程序重用,而这时再使用新event的srcElement属性,将导致使用无效的CTreeNode对象地址,从而取到不正确的CElement对象地址,使CElement::Doc函数的调用出现异常。
4. 使用精心构造的网页代码,可以在上面的CTreeNode被释放后将原内存内容修改为特定的值,结合heap spray填充相应区域,可以使得CElement::Doc函数调用特别构造的shellcode代码,从而达到以浏览器进程的权限远程执行任意代码的目的。