| 关键词: 函数 nbsp 漏洞 对象 指针 地址 shellcode 浏览器 缓冲区 内存 |
本文对历史上的微软IE 浏览器的影响较大0day做了梳理,讨论了IE漏洞在攻击防御技术上的进化,以及记录了此类漏洞前人们在历史上遗留下来的对抗经验和足迹。 在IE浏览器攻防已经白热化的进入到第三个阶段的时候笔者才进入到IE浏览器攻防方面的研究学习。此时代号’Project Spartan’的微软的Edge浏览器从IE11手中接过windows默认浏览器的重担,使得服役将近20年的IE浏览器定格11这个历史的大版本号上面,自觉对IE浏览器漏洞的历史研究应有一篇简记,可供后来的初入行的安全研究者有所参考,故成此文,疏漏之处再所难免,敬请指正。 0×01 浏览器漏洞研究的前置背景 最近几年,网络安全研究的部分重心开始有由PC端向移动端倾斜的趋势。但是在PC端的安全研究依然以浏览器IE/Chrome/Firefox/Spartan,Adobe的Reader/Acrobat/Flash系列作为技术研究深度的展现。当然还有部分用于特定地区定向攻击的文件格式漏洞也属于一些黑客着重关注的领域,比如日本比较流行的字处理软件ichiaro和在韩国地区比较流行的Hancom Office等字处理软件系统以及WPS等字处理软件方面的文件格式漏洞。 浏览器从诞生之初主要提供简单的文档阅读功能.很少构成网络安全威胁,但随着互联网的高速发展,越来越多的功能集被加入到浏览器中。浏览器不仅需要像操作系统那样,为阅读文档、观看电影、欣赏音乐等传统计算机应用提供基础,也需要为社交网络、网络购物等新兴互联网应用提供支持。浏览器在增加功能集的同时,也就带来了更多的安全问题。而集成捆绑于系统中的IE浏览器可以占据市场较大份额,自然也成了众矢之的。针对IE浏览器的攻防军备竞赛也就是在这种情况下拉开了帷幕。 0×02 IE浏览器漏洞攻防的几个时代 1.1 缓冲区溢出和ActiveX控件时代(03年-08年) 03年-08年这是一个阶段,这个阶段时候的IE漏洞基本是以ActiveX控件造成的漏洞居多以及栈溢出漏洞还有一些简单的堆溢出漏洞。比如IFRAME标签的单个超长SRC属性导致的缓冲区溢出以及类似的栈溢出漏洞。 早一些的阶段,常规的Fuzz 方法,无论是基于变形的还是基于生成的,比较适合应用于二进制格式的流数据,特别是那些包含大量C 语言结构类型的文件或网络协议格式。由于格式解析代码经常不加检查的使用数据流数据作为内存操作的参数,单点的畸形往往就足以触发解析代码中可能存在的处理漏洞:超长数据导致的缓冲区溢出、畸形数值导致的整数上溢和下溢、畸形索引值导致数组的越界访问、畸形记数导致过量的内存读写操作。其中对于超长数据导致的缓冲区溢出,样本构造起来相对简单,传入超长的值。所以这种类型的漏洞由于发掘起来相对简单。 对早期的IE 0day 漏洞的历史简单做一下梳理,只包含了影响比较大的例子(图1.1.1,图1.1.2),有些漏洞可能并不是IE 本身的问题,但是以IE 为最主要的利用渠道。 图1.1.1 图1.1.2 1.2 时代关键字 防护方: DEP /ASLR / Stack Cookie 攻击方: 栈溢出/简单堆溢出/ROP/HeapSpray A 缓冲区溢出 关于缓冲区溢出也简单给出一个例子方便理解: 图1.2.1 我们在VC6.0的编译器编译上述程序,执行后结果如下: 图1.2.2 可以看到main函数中只调用了foo这个函数。但是实际运行中,bar函数也被调用了。 实际上因为foo函数处理不当,并且外部输入超长,造成了缓冲区溢出后修改了foo()函数的返回地址从而导致程序执行本不应该执行的bar()函数。而如果被覆盖的返回地址是一串经过精心编码具有后门功能的shellcode,此时计算机即可被恶意攻击者控制。 这只是一个简单的C程序的范例,表现在浏览器当中形式上多少有所不同,比方说IE浏览器支持的IFRAME标签,IFRAME标签的单个超长SRC属性构造超长数据可能就会导致的缓冲区溢出从而浏览器崩溃。 DEP和Security Cookie 从上面的缓冲区溢出我们可以看到,在特定的环境下只要控制输入的数据超长然后覆盖掉返回地址,只要此时程序崩溃就很容易感知目标程序存在缓冲区溢出漏洞。然后在修改这串超长数据糅合上恰当的shellcode精确覆盖返回地址就可以执行我们的恶意代码。只是留给攻击者的这样的大好时光极为短暂。微软从Windows XP SP2开始提供DEP的支持。DEP全称是Data Execution Prevention,可以分为硬件的DEP和软件的DEP。但是目的都是一致的。阻止数据页上代码执行。(图1.2.3) 图1.2.3 由于数据所在的内存页被标识为不可执行,即使程序溢出成功转入shellcode的执行,这个时候CPU就会抛出异常从而阻止恶意shellcode的执行。 从这里也可以看到这一阶段的攻击者只是去覆盖栈上的返回地址,试图从栈上将恶意shellcode执行起来。 考虑到攻击是因为覆盖返回地址产生的,微软在VS2008和之后的编译器加入了一个编译选项GS。也就是Security Cookie也可以称为Stack Cookie。 图1.2.4 可以看到在开始的时候会将一个security_cookie提前写入到栈中。而在函数返回之前会检查这个security_cookie是否被篡改。 图1.2.5 一旦被篡改便会跳转到异常执行的流程: (图1.2.6) 当然这里说的是栈中的情况。在堆溢出中也有相似的防护措施如Header Cookie。 盯上SEH 当攻击者发现覆盖4字节返回地址(32位系统)去执行shellcode这种攻击方法的门槛被抬高之后便又想出了新的方案。覆盖SEH的Exception handler 其实在二进制的攻防当中所有的努力都是为了获得哪怕一次控制EIP(RIP)的机会。而SEH恰好符合这个要求可以给攻击者提供这个机会。SEH存放在栈内,故超长的数据就可能覆盖掉SEH ,其中将异常处理函数的入口地址更改为shellcode的起始地址。由于溢出后产生的错误数据往往会触发异常,而此时shellcode恰好可以得到一次被EIP指向的机会。 只是留给攻击者的好时光依然极为短暂。在VS2003(.net)当中支持了/SafeSEH的选项,用于应对针对S E H的攻击。后来又进一步推出了SEHOP。当然这些措施需要XP SP2操作系统以及更新的操作系统还有DEP的配合。这两种防护措施详细展开又要占很多篇幅。感兴趣的读者可以自行学习。 需要说明的是64位的windows系统SEH已经不是放在栈中了。想要通过栈溢出覆盖异常处理例程来实现漏洞利用已经是不可能的了。 ROP ASLR和HeapSpray技术 前面说到DEP技术即使返回地址被shellcode覆盖,DEP也会去阻止shellcode的执行。但是如果执行的代码是操作系统的库本身提供的函数如直接使用libc库中提供的system()函数来覆盖程序函数调用的返回地址。然后传递重新设定好的参数使其能够按照预期执行。这种绕过DEP的攻击方式称为return to libc。 Return-to-libc 攻击用库函数的地址来覆盖程序函数调用的返回地址,这样在程序返回时就可以调用库函数从而使攻击得以成功实施。但是由于攻击者可用的指令序列只能为应用程序中已存在的函数,所以这种攻击方式的攻击能力有限。并且攻击只能在 x86 的 CPU 平台中实施而对 x86_64 的 CPU 平台中无效。这是因为x86_64CPU 平台中程序执行时参数不是通过栈传递的而是通过寄存器,而 return-into-libc 需要将参数通过栈来传递。 由于这种 return-to-libc 攻击方式的局限性,返回导向编程(Return-Oriented Programming, ROP)被提出,并成为一种有效的 return-to-libc 攻击手段。返回导向编程攻击的方式不再局限于将漏洞程序的控制流跳转到库函数中,而是可以利用程序和库函数中识别并选取的一组指令序列。攻击者将这些指令序列串连起来,形成攻击所需要的 shellcode 来从事后续的攻击行为。因此这种方式仍然不需要注入新的指令到漏洞程序就可以完成任意的操作。同时,它不利用完整的库函数,因此也不依赖于函数调用时通过堆栈传递参数。 一般我们通过immunitydbg配合mona的脚本插件提取rop的指令序列构造rop chain。 Rop chain 展示: 图1.2.7 Rop的出现一度使得在XP时代的攻击者占据上风。由于dll加载地址的固定。针对不同的IE版本然后提取rop chain的工作虽然让人感觉无趣但是达成的攻击效果的确不错。但是作为防御一方的微软的脚步并没有停止。Vista系统的臃肿繁杂为人所诟病并且市场份额也并不高。但是从Vista系统引入的由Win7沿袭的ASLR机制却结结实实的又一次提高了攻击的门槛。 在Rop攻击中,攻击者可以事先预知特定的函数如system或者VirtualProtect的入口地址。这是因为在XP以及2000的操作系统上面,由于kernel32.dll这些动态链接库加载地址是固定的,所以导致相关的函数入口地址也是固定的即攻击者可以事先确定这些函数的入口地址。 当然Rop这一技术有一个弊端就是针对不同的操作系统要编写提取不同的rop chain。这使得兼容性并不是很好。 ASLR全称Address space layout randomization,是系统级别的特性,率先在Vista操作系统中得到支持。它的原 理就是在 当一个应 用程序或动态链接库 ,如 kernel32.dll,被加载时,如果其选择了被ASLR保护 ,那么 系统就会将其加载的基址随机 设定 。这样 ,攻击者就无法事先预知动态库,如 kernel32.dll的基址,也就 无法事先确定特定 函数,如 VirtualProtect的入 口地址 了。 如果感兴趣,可以自己写一段简单的C代码打印出VC运行库的加载地址。会发现每次重启之后win7下面VC运行库加载地址是变化的,但是XP系统VC的运行库加载地址就是固定的。 Heap Spray ASLR在新系统上面的应用又使得相当长的一段时间在缓冲区溢出利用时代攻击方陷入了弱势。但是攻击者发现之前很早就被提出的(2001年左右)heap spray正好可以解决这个问题。基本在2005年之后IE漏洞的利用很多都用到了Heap Spray的技术。 在缓冲区溢出的时候,我们能够劫持覆盖一个地址,从而使得程序崩溃,但是只使得程序崩溃这样是没有价值的接下来如何将程序的执行流程交接到shellcode的手中,这就变成了一个问题。如果覆盖到一个固定的地址比0x0C0C0C0C,0x0A0A0A0A,0x0D0D0D0D而恰好从这个地址开始布满了我们的shellcode。这样触发漏洞的时候就转入了我们的shellcode进行了恶意代码的执行。 实际应用当中shellcode前面都是要加上一些slidecode的(滑板指令)。为什么要加入滑板指令而不是全用shellcode去填充呢。因为如果要想shellcode执行成功,必须要准确命中shellcode的第一条指令,如果整个进程空间都是shellcode,反而精确命中shellcode的概率大大降低了,因为必须要命中第一条指令,加上slidecode之后,现在只要命中slidecode就可以保证shellcode执行成功了,一般shellcode的指令的总长度在50-100个字节左右,而slidecode的长度则大约是100万字节(按每块分配1M计算),那么现在命中的概率就接近99.99%了。因为现在命中的是slidecode,那么执行slidecode的结果也不能影响和干扰shellcode。但是如果单纯使用0×90(NOP)指令进行填充,因为现在使用较多的攻击场景是覆盖虚函数指针(这是一个多级指针),这种情况下如果你使用0×90来做slidecode,而用0x0C0C0C0C去覆盖虚函数指针,那么现在的虚表(假虚表)里面全是0×90909090,程序跑到0×90909090(内核空间)去执行,直接就crash了。根据这个流程,你可以看到,我们的slidecode也选取0x0C0C0C0C就可以了。 (图1.2.8) 大概大量分配内存之后分别覆盖到的地址是这样的: 0x0A0A0A0A(160M), 0x0C0C0C0C(192M), 0x0D0D0D0D(208M) 网马里面进行堆喷时,申请的内存大小一般都是200M的原因,主要是为了保证能覆盖到0x0C0C0C0C地址。(图1.2.9) 2.1 UAF时代 由于缓冲区类漏洞由于发掘起来相对简单,在攻防对抗的时间长河中这类漏洞资源很快耗尽。08年之后释放重利用这样漏洞利用方式变成了IE漏洞的主流。逐渐在这几年达到了高峰。对象畸形操作类的漏洞一般来说触发漏洞需要一系列的操作。单个的操作,比方说对象的创建使用删除都是正常的。导致问题的是对于对象操作的畸形的组合。由于没有标准的章法可供参考,基于传统的溢出类漏洞的发掘手段已经不甚适用。 2.2 对象操作类漏洞原理 跟面向过程的编程语言不同,c++支持多态和继承。支持这些机制的核心就是虚表。C++的(虚)函数指针位于一个全局数组中,形成虚表。而指向这个虚表的指针(VSTR)一般位于对象实例在内存中开始的4个字节(32位系统). 之后才是类中声明的数据成员,一般按照声明的先后顺序排列。对于存在多态行为的类,子类的所有实例共享相同的虚表,但区别于父类的虚表。对于某个对象,其调用存在多态行为的某个函数时,会先通过虚表指针得到虚表.再根据函数在虚表中的偏移来得到相应的函数指针,最后才会调用函数。 另外,对象所在的地址一般通过ecx等寄存器传递。因此.C++中调用存在多态行为的函数的反汇编代码类似于如下序列: (图2.2.1) 我们以stackexchange上面Polynomial 给出的示范代码对UAF做一下简介。 如下例的这样的一段C++代码: 图2.2.2 可以衍生为: 注意,当执行到Account_GetBalance的时候,由于Account_Destroy 函数的执行myAccount指针指向的内存已经是一个不确定的状态。如果此时能够可靠的触发Account_Destroy函数。并且填充一块精心构造的内容到myAccount指针指向的内存,时机在Account_Destroy执行后Account_GetBalance执行之前。很多情况下这是可能实现的。 Account_Create函数执行之后分配了8个字节的内存。Balance和transactionCount分别占据4个字节,并且返回一个指向他们的指针。这个指针储存在myAccount变量当中。Account_Destroy释放了这块内存,但是myAccount变量依然指向那个8字节的内存。我们将39 05 00 00 01 00 00 00 这8字节内容可靠的进行内存分配。由于旧的8字节内存块已经被标记释放,所以内存管理器有很大可能会用新分配的内存去覆盖掉旧的内存块到那8个字节已经被释放的内存。这个时候Account_GetBalance函数被调用了,他会去读取那8个字节的内存块,但是实际上那8字节的内存块已经被我们覆盖成了 Balance 39 05 00 00 (1337) transactionCount 01 00 00 00 (1) 所以我们已经越权执行到了下一个函数。 当然具体到IE当中,由于对象繁杂,UAF就更为错综复杂。 浏览器中跟对象操作类漏洞相关的对象有DOM ,BOM ,JavaScript对象。我们以DOM对象的分配过程为例。 DOM(文档对象模型)提供了操作HTML/XML文档的接口。IE浏览器中跟DOM实现相关的代码主要在mshtml dll中。mshtml中的CMarkup类负责构造整个htmI树结构,其成员函数CreateElement会调用全局的CreateElement函数束构造不同标签对应的元素。比如 |