这是我一年多以前刚刚开始接触浏览器漏洞时写的一篇小总结,之前发到过安全客上。这里转发一遍分享给大家
前言
在前一部分中,我们介绍了从IE6到IE11的堆喷射方法,其实堆喷射只是一种手段目的还是为了实现最后的漏洞利用。在这一篇文章中,我们会根据时间顺序来讲解IE浏览器漏洞利用技术的发展,我们会把主要精力放在UAF漏洞的利用上面,同时会涉及一些其它类型的漏洞作为辅助以绕过漏洞缓解措施,这篇文章主要介绍技术发展脉络,实际的漏洞调试会放在下一章,期间我阅读了大量的文档、会议PPT和博客文章,在此也对前辈们的分享表示感谢。同时给大家分享两句我很喜欢的话,一句来自大家应该都很熟悉的陈皓大牛,一句来自charles
petzold大家应该都有读过他的书
要了解技术就一定需要了解技术的历史发展和进化路线。因为,你要朝着球运动的轨迹去,而不是朝着球的位置去,要知道球的运动轨迹,你就需要知道它历史上是怎么跑的。
学习技术发展史的重要意义正在于此:追溯的历史越久远,技术的脉络就变得越清晰。因此,我们需要做的就是确定某些关键的历史阶段,在这些阶段,技术最天然、最本质的一面将清晰可见。
首先简单介绍一下本篇文章将要提到的漏洞类型,我们这篇文章主要会针对两类漏洞进行描述,即越界访问漏洞和释放后重引用漏洞。这两种漏洞的英文名称分别为Out-of-Bound和Use-After-Free,因此我们通常简称这类漏洞为OOB和UAF。
1.越界访问漏洞
越界访问漏洞,个人认为越界访问漏洞应该是按照漏洞造成的效果进行划分的,而不是依照漏洞成因进行的划分。我认为如果是从成因的分类上来讲,像堆溢出、整数溢出、类型混淆等都可以造成越界访问漏洞。
越界访问,所谓的访问就是指越界读和越界写,在后面我们可以看出越界漏洞是比较容易利用,也是比较好用的一类漏洞,这里好用指的是效果比较好,一般通过这种OOB漏洞可以在IE浏览器中轻易的实现绕过ASLR的保护。
而这种漏洞的利用,一个共通点是要进行内存布局(或称为Feng shui技术)。即把一些特殊的对象或者结构布置在漏洞对象附近,否则读写都无从谈起,我们会在后面展开来说这些内容。
2.释放重引用漏洞
Use-After-Free漏洞中文名为释放后重引用漏洞,这种漏洞估计大家都比较熟悉。
所谓的“释放重引用”是指在一块内存被释放后在某处还保存有指向这块内存的悬垂指针,之后对悬垂指针进行了解引用(所谓的重引用)导致了漏洞的触发。
我们分析UAF漏洞一般要搞清楚几个关键点:
1.是什么对象发生的UAF?
2.UAF对象何时被分配、何时被释放?
3.导致crash的流程是什么?
4.为什么会存在悬垂指针?
但是在早期的UAF漏洞利用来说,一些Hacker们往往只需要知道步骤2、3就可以实现漏洞利用。这是因为早期的UAF一般都是通过占位和堆喷射来进行利用的,比较简单粗暴。
1.为什么IE中会存在大量的Use-After-Free漏洞?
我们的第一个问题是IE浏览器中为什么会爆出大量的UAF漏洞?
这个问题的提出并不奇怪,因为其它的软件诸如Office Word的漏洞可能基本上都是一些堆栈溢出,而UAF则是凤毛麟角。
IE浏览器中并不是没有存在过栈溢出这些类型的漏洞,而是经过了十余年的发展基本都已消耗绝迹,但是UAF漏洞“络绎不绝”的本质原因在于IE浏览器中存在着大量的各种类型的对象和其间互相关联的关系,包括各种标签和各种内部数据结构比如CElement就是代表元素对象的父类,举例来看CButton是CElement的子类代表<Button>标签。
这些html标签和DOM
Tree是由IE浏览器中的渲染引擎mshtml(就是所谓的Trident)负责解析的。html标签在mshtml内部就是由一个个的C++对象来进行表示的,同样DOM树也是通过一些数据结构来进行描述(比如CTreeNode、CTreePos),这些对象之间存在复杂的相互关系。并且mshtml使用了引用计数的方法来跟踪对象的使用情况。
我们可以通过泄漏的IE5.5源代码来观察这一点,虽然IE5.5版本对于我们来说已经是相当的古老了,但是其实一些核心的部分还是相当相近的。
如下是IPrivateUnknown接口,这个接口在我们源码中存在着如下的继承关系CElement->CBase->IPrivateUnknown。
在此我推荐阅读以下两篇文章,可以增进对IE浏览器的了解:
《IE安全系列:IE浏览器的技术变迁(上)》
http://www.infoq.com/cn/articles/Internet-Explorer-Security1
《IE安全系列:IE浏览器的技术变迁(下)》
http://www.infoq.com/cn/articles/InternetExplorer-Security2
2.如何利用IE中的Use-After-Free漏洞
如何利用UAF漏洞是我们这篇文章的主题,在分门别类的进行讨论之前,我们首先介绍一些基础知识。我们在前面介绍了UAF漏洞的一些信息,对于UAF漏洞的利用无论在什么时期,一个通用的步骤就是在UAF对象被释放之后马上去分配一个相同大小的内存块,我们称这一步操作为“占位”。占位的原理在于堆分配的机制,当一块堆内存被释放后出于效率的考虑会被保存在一些结构中以便于再次的分配。占位就是利用这一点,通过分配相同大小的堆内存试图重用UAF对象的内存,对Linux堆有了解或是打过CTF的同学应该都比较熟悉这一点了。为了成功实现占位,一般是多次分配相同大小的内存以保证成功率。
需要说明的一点是,不是所有的UAF漏洞都是可以利用的,因为一些漏洞无法进行占位。比如有的漏洞它的对象释放和重用操作就在同一个函数中,刚刚释放完马上就重用了,这种情况根本没有机会去进行占位,从而无法进行利用。
我们说了这么久的占位,其实占位的目的是为了控制对象的内容。同样目的的操作,还有挖坑法(make
hole),挖坑法是指在布局好的内存中释放一个指定大小的块,好让目的对象落在我们布局的内存中,我们会在后文提到这一点。还有内存未初始化漏洞的利用也与之类似,内存未初始化漏洞是指分配一块内存后未经初始化就直接进行使用,我们为了控制未初始化对象的内容会先释放一些与之相同大小的已布置好内容的内存,然后让未初始化对象来重用我们的内存。
接下来我们就根据历史发展来讨论利用技术,可以看出随着历史的向前漏洞利用技术有了很大的发展,相比早期的利用技术现在无论是在思路还是在手法上都是发生了质的飞跃。
3.IE6漏洞利用(史前时代)
我们把IE6作为IE漏洞利用的开端,我们称之为史前时代,说到史前我们可能会想到刀耕火种、茹毛饮血。此时的IE漏洞就是处于这样一个野蛮生长的时代,漏洞利用技术简单粗暴导致网马大量横行。主要原因在于IE6时代的浏览器版本不支持DEP等漏洞缓解措施(虽然此时操作系统已经支持DEP),导致漏洞利用的成本低廉,但是也因此流传下来一些“远古神话”,比如经典的0x0C0C0C0C。
这一时期由于Active X插件作者的水平参差不齐,因此大量的控件存在有诸如栈溢出之类的简单漏洞,利用方式也极为简单粗暴配合堆喷就可以实现利用,这些漏洞我们简单略过不再详述。
我们把关注重点放到此时的UAF漏洞上面,通过我们前面对UAF漏洞的简单介绍就可以看出,UAF漏洞与栈溢出有着本质不同。栈溢出可以简单直接的控制返回地址从而劫持执行流程,但是UAF往往是处于堆上并没有直接劫持流程的途径。为了能够在堆上劫持指令执行流程,前辈们想出了劫持虚函数调用的方法。
我们首先简单介绍一下虚函数,在C++程序中,如果一个类存在虚函数那么当这个类实例化对象后,对象的前4个字节就是虚函数表的指针,当我们调用虚函数时实际上是到虚函数表中寻找函数指针。接下来,我们就通过这一点来进行利用。
我们首先通过占位来控制UAF对象的内容(如果你不理解这一步,可以往前看一看),控制了对象的内容就相当于控制了虚函数表的指针。接下来我们需要一个稳定可达的地址,因此我们使用上一篇文章讲过的堆喷射,然后把虚表指针指向我们喷射的内存地址,这样一旦触发漏洞就会把我们的喷射内存当作虚表来执行了。
为什么一触发漏洞就会把喷射内存当作虚表来执行呢?如果没有调试过IE漏洞可能会提出这个问题,因为我们实际去调试漏洞就会发现事实上UAF漏洞触发后基本都会crash在虚函数调用处,如果你发现windbg停在一个莫名其妙的地方很可能是因为没有开启页堆,可以使用!gflags.exe
-i
+hpa进行开启,关于页堆可以学习一下张银奎老师的《软件调试》。(此外新手可能会发现明明异常了却没有停下来,可能是没开启子进程调试,主要是对于IE8以后这种多进程浏览器来说。)
根据我们上面的描述可以看出,我们是在把喷射的内存当成虚表。但是当我们调用虚函数时,往往是下面这个样子的:
这两条指令意味着,喷射的内存不仅会被当成虚表还会被当成指令来执行。并且更糟糕的情况是:这里我们不能确定堆喷射的准确分配地址,就是说我们不能确定堆表指针到底喷射在哪里。这时就对我们的喷射提出了要求,我们需要寻找一个既可当作地址解释又可以当作无意义指令解释的值。
在这种情况下“上古传说"0x0C0C0C0C就诞生了,如果我们使用0x0C0C0C0C作为喷射的内容的话,当mov
eax,[ecx]时就会取到0x0C0C0C0C作为指针来进行跳转,call
[eax+4]会把0x0C0C0C0C的0x4处偏移取出并call,当然其结果依然为0x0C0C0C0C,这样call
0x0C0C0C0C会执行指令0x0C,而0x0C相当于nop就会最终执行到shellcode了。当年有很多这样的通用地址存在比如0x0D0D0D0D、0x06060606。
这种利用方式简单粗暴却又有效,因此我们称为史前时代,就像TK教主说的一样:
在当年无 DEP 的环境下,几乎完全不懂漏洞原理的人,知道去 Heap Spary 0x0C0C0C0C 就能搭积木一样写出 Exploit,而且还很通用。
当然,对于不涉及虚表访问的利用来说,使用0x0c0c0c0c是完全没有意义的。不过这个地址已经成为一个"上古神话"了,所以我们还是会经常看得到它,甚至于一些安全软件一旦发现这个值就会报警。
4.IE8早期漏洞利用(石器时代)
前面我们说IE6时期是史前时期,因为那时的漏洞利用简单粗暴。相比于当时,IE8时代的利用技术向前走了一大步,不过因为安全体系的问题早期的IE8利用依然只能称为是石器时代,还是相当的原始。
自IE8开始,DEP和ASLR成为浏览器中默认启用的缓解措施,如何bypass
ASLR和DEP成为了攻击者首要面对的问题。我们简单介绍一下DEP和ASLR,如果是熟悉Linux的同学,那么Linux下的NX保护与DEP是很类似的,都是把一些非代码段内存设为不可执行来阻止攻击者运行shellcode。在其它的软件中bypass
DEP通常使用ROP技术,但是由于IE几乎都是基于堆的漏洞不存在直接进行ROP的条件所以并不能通过简单的ROP实现bypass
DEP。而ASLR会使得模块装载的地址变得不确定,对漏洞利用有一些了解的同学肯定知道Rop技术是依赖于一些rop
gadgets来进行不断的跳转利用的,ASLR的启用会直接妨碍我们获取rop gadgets。
不过对于早期的利用,ASLR并没有对利用者造成太大的困扰。因为ASLR早就在诸如Office
Word之类的软件中启用了,Hacker们直接套用了在这些软件中的做法即利用一些未开启ASLR的模块进行利用。因为当时很多的模块并不支持ASLR因此加载在固定基地址。这种方法也是比较简单粗暴的而且通用性比较差,比如以前在IE中常用的Java6的msvcr71.dll,如果目标并没有安装JRE或者版本不对利用都不能成功。
因为此时仍然比较原始,所以我们称之为石器时代。事实上,今天的应用程序不支持ASLR的已经非常少见了,想通过不支持ASLR的模块来实现ROP已经不大可能了。所以我们把完整的利用方法放到下一章中详述。
5.IE8浏览器结合信息泄漏利用(铁器时代)
据说人类跟动物的区别是人类会使用工具,那么这一时期利用技术的进步堪比从石器进化到铁器。
这一时期的标志事件是Peter
Vreugdenhil在Pwn2Own2010中攻破IE8项目,这一过程中的技术手段对后来的利用技术发展有着重要的作用。Peter
Vreugdenhil利用IE8的手段是把一个OOB漏洞与一个UAF漏洞相互结合,我们首先来说OOB漏洞。Peter
Vreugdenhil通过内存布局把BSTR布置在存在OOB的对象后面,目的是进行信息泄漏,通过越界写来改变BSTR的长度,实现了越界读。
我们在前面说过BSTR不是简单的Unicode字符串,BSTR的结构由4字节的长度(size)域、2字节的结束符(x00x00)加上Unicode字符串构成。通过我们精心构造内存布局,使BSTR对象紧随漏洞对象的后面。之后再在BSTR后面再放置目标对象,这样当触发漏洞对象发生越界访问的时候就可以覆盖掉BSTR结构的size域。一旦我们把size域覆盖为更大的数值,我们就能够使得BSTR发生越界读(因为BSTR只可读不可写)。然后通过js脚本读取BSTR字符串,就能够读到BSTR之后的对象。我们的目的是获取后面对象的虚表地址(首4个字节)。如果你想了解的更详细可以参见(http://vreugdenhilresearch.nl/Pwn2Own-2010-Windows7-InternetExplorer8.pdf )
为什么获得虚表地址就可以bypass
ASLR呢?因为对于C++程序来说虚函数表是被编译在全局数据段的,就是说对于模块的基地址的偏移是固定的。我们通过泄漏的虚函数表的地址减去偏移就可以知道对象所处的dll模块的基地址,也就可以使用这个模块中的gadgets了。
这种方法有两个需要解决的问题:第一是如何构造稳定的内存布局使我们上述的内容得以实现。第二是当我们覆盖成功后,如何通过javascript脚本层面上的操作把值获取到。其实我们后面要讲到的方法都面临着这两个问题。
由于这种利用较为简单,可以直接参考泉哥的著作《漏洞战争:软件漏洞分析精要》里面第三章的CVE-2012-1876的利用分析,其使用的方法就是通过BSTR进行泄漏,我们也会在下一篇中给出实际的漏洞调试。
单单绕过ASLR是无法实现漏洞利用的,因为DEP的存在我们没有办法在堆上执行指令。为此Hacker们想了很多办法,其中我认为最早实现成功利用的依然是Pwn2Own
2010上Peter Vreugdenhil使用的方法,虽然与我们这里讲的不完全相同,但是我觉得是Peter
Vreugdenhil方法的进化版。我们忽略Peter Vreugdenhil的方法(感兴趣的可以查看上面的连接),我们使用的手段是stack
pivot,所谓stack pivot就是通过mov esp,eax、xchg esp
eax等指令来实现把栈转移到堆上,因为一般的UAF漏洞触发时我们都可以控制至少一个寄存器的值。通过把esp指向我们喷射的内存,我们就可以把堆伪造成栈,从而像普通的栈溢出一样进行ROP,通过执行ROP最终实现代码执行。
但是这一利用方法首先要确保的是我们要能够精准的计算堆喷射的地址,因为堆不可执行所以我们不能再依赖于用于缓冲的nop指令了。好消息是我们在前一篇文章中已经讲过准确计算的原理和实现了,这里再简单复述一下。当我们大量分配堆块时可以发现地址的最低几位是一直不变的,地址改变的熵只是固定的地址高位并且堆块的分配相当的稳定。这样如果我们使偏移都落在地址的高位,那么我们的指向就会是整块进行偏移,从而保证了每次指向的都是计算好的准确的地址。
举个例子:我们可以以每个块为单位计算出ROP链第一条地址的偏移,然后其实我们可以想一下0x0C0C0C0C这个地址还有没有用?在这种利用环境下,第一不需要跳板指令,第二我们跳转目的地址是精确的,那么0x0C0C0C0C这种地址就根本没有存在的价值了。我们要的就只是一个堆喷射可达的稳定的地址。
无堆喷射,通过ANIMATECOLOR对象实现利用
这种方法不需要进行堆喷射就可以实现利用,堆喷射其实并不能说是一种优雅的利用方法,因为分配内存需要一定的时间,而且如果目标机器的配置较低的话可能会导致卡顿从而被目标察觉。我之前在binvul上看到过一些所谓的“不弹不卡不喷射不风水”的样本其实指的就是这种技术。
ANIMATECOLOR是IE8版本起提供的一种对象,由于这种对象的特殊构造所以可以不使用堆喷射来实现利用,我们在下一篇实际漏洞调试时再来进行分析。
6.结合Flash的利用(中世纪)
到这里浏览器利用技术又是一个飞跃,结合flash利用虽然不能说特别优雅(因为要依靠第三方),但中世纪是文艺复兴的先声,可以说自此之后利用技术又进入了一个发展的新巅峰。
这种利用技术不是来自于Pwn2Own也不是来自于某次会议的分享,相反,随着时间的发展,在2013年网上流传出了一些无需多漏洞结合使用,通过单一漏洞就可以bypass缓解措施+执行代码的exp样本,这些exp样本应该是用于实际攻击的武器。其主要特点是结合了flash进行漏洞利用,这种利用技术最早应该是由李海飞前辈在《Smashing
the Heap with Vector:Advanced Exploitation Technique in Recent Flash
Zero-day Attack》这文章中提出的(CVE-2013-0634)。
就像李海飞前辈所说,这完全是一种新技术。并且这种新技术可以只凭借一个单一的漏洞实现bypass全部的缓解措施并且执行最终的shellcode,这一点是以前的exploit所做不到的事情。
我们简单的概括一下利用的方法,我们先忽略漏洞的细节简单的认为它是一个0x90个字节的堆块发生的溢出。我们首先分配一系列0x90大小的Vector对象,对于储存数字的Vector来说每个数字占8个字节,16个数字加上16字节的固定结构正好满足0x90的大小。
我们在vector对象布置完成之后,通过代码来释放一些0x90大小的vector,再触发漏洞。之后会分配具有溢出的0x90大小的堆块,因为尺寸与我们之前释放的vector尺寸相同,根据堆的特性漏洞堆块会重用我们之前释放的vector对象内存。这一步操作称为挖坑(make
holes),挖坑的目的是为了使得漏洞堆块处于vector对象的包围之中。
下一步,我们只需要利用溢出就可以覆盖掉相邻vector对象的“Number_of_elements”域。
覆盖的结果是使得相邻的vector可以发生越界访问,通过操作这个越界的vector我们又可以覆盖下一个vector的“Number_of_elements”域,但这次我们可以直接把“Number_of_elements”域改的很大,从而实现了整个进程地址空间的任意读写。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)