感觉写的太水了,有空一定重新写一份更详细明白的。。
堆喷射技术已经发展很多个年头了,相关的研究也挺多的,虽然现在Win7、Win8系统下的利用越来越难,但是该技术仍然是可用的,比如最近新出的IE8 use after free漏洞(CVE-2013-1347),Metasploit里的利用代码就是使用堆喷技术及ROP实现Shellcode的布置和绕过DEP防护的,网上有关的资料不多,0Day2书中有关的内容并不多,只是简单介绍了HeapSpray技术,并没有讲解如何结合ROP绕过DEP防护,在看雪上有讨论过这个漏洞在IE8上的利用,我尽量把做的过程写的详细些,细节和学习过程遇到问题的解决方案都做一下说明,本人很菜,写的比较啰嗦,也是因为自己学习的时候没有找到详细的资料,高手飘过,如果有问题欢迎交流,希望能对学习漏洞的人有所帮助(工具及资料都会打包提供下载)。
一、 简介
在漏洞的利用过程中,攻击者通常利用溢出控制程序的执行过程,用来执行布置在堆栈中的ShellCode,为了应对这种威胁,微软开始在XP SP2及之后的系统中使用DEP防护措施,以禁止进程将栈和默认堆中的数据当做指令来执行,这样在堆栈中的ShellCode执行之前程序就会异常终止,IE8及之后的IE版本才支持DEP防护,WinXP默认安装的是IE6(现在很不安全),最高支持IE8,Win7系统默认安装的是IE8,Win8系统则默认安装IE10,本文主要针对WinXP的各个版本的IE及Win7系统支持DEP防护的IE8浏览器实现绕过DEP的漏洞利用:
(图1.1:IE8浏览器的DEP防护)
这种防护可以说是目前最强力的防护措施(尤其是在结合ASLR:布局地址随机化技术之后),相应的绕过措施也有很多种,基本都是利用ROP链执行VisualProtect、VisualAlloc等函数将ShellCode布置所在内存属性设置成可执行的,或是设置关闭DEP的参数调用ZwSetInformationProcess函数等,本文中采用ROP链调用VisualProtect函数将ShellCode所在的内存属性设置成可读写执行的方式,绕过DEP防护。
ROP链:
简单的说就是一段返回地址序列(注意不是可运行的指令,而是存放的返回地址),由于不能直接执行堆栈里布置的Shellcode数据,所以目前一般调用程序所加载的DLL模块里(ollydbg的M内存窗口可以查看加载的DLL信息)的代码达到变相执行我们的ShellCode的目的,类似于从DLL里的执行指令中拼凑出ShellCode的功能,比如A.dll里调用一部分代码,再跳转到B.dll里执行一部分代码,整体组合起来达到执行ShellCode的目的,但是将整个ShellCode拼凑出来难度很大,许多ShellCode指令对应的机器码不一定能够找到,所以一般是利用ROP执行关闭DEP的指令,再继续跳转到堆栈内的ShellCode执行,这样需要的ROP链的长度就可以大大降低了(图1.2):
图1.2一段使用immunitydbg的插件自动获取的ROP链,由一串地址构成,右边有对应地址处指令的注释,每次执行retn时会继续执行下一条返回地址的代码(retn是把栈顶数据弹出到EIP执行,同时ESP自加,指向下一条地址,上一条指令retn执行之后就会跳转到esp指向的下一条地址处的指令)。
HeapSpray(堆喷射)技术:
由于传统的堆栈溢出利用方式受到了微软防护措施(GS、SafeSEH、ASLR等)的限制,对于浏览器或是PDF阅读器等可以用javascript分配内存的程序,攻击者可以通过自定义的代码控制堆内存分配,将ShellCode布置到堆内存中,在发生溢出后劫持程序EIP跳到堆中执行,举个例子:用空指令Nops(PS:对程序的执行没有影响的指令,一般是nop指令,也可以是or eax 0c之类的对使用的关键寄存器影响不大的指令等)填充在ShellCode之前直到能分配1M的内存空间,连续分配200次,由于堆中空间是0地址从下向上布局的,所以程序领空的50M-200M堆空间一般会被自己的Nops+ShellCode堆块覆盖,如果劫持程序到地址0x0c0c0c0c、0x0d0d0d0d之类的指令开始执行的话,一般会先执行Nop指令,最终执行到ShellCode,所以堆喷并不用于漏洞发现,而是为了达到稳定布局ShellCode的目的,算是一种漏洞利用方式。
这次试验中,以超星阅览器V4.0为例,来演示Heapspray及ROP技术的使用方式,相关的漏洞成因及IE6下无DEP防护的漏洞利用网上已经讲了很多,0day漏洞第二版、看雪自己举办的exploit竞赛里都有这个例子,控制程序执行流程比较简单(图1.2),溢出后的跳转地址为0x0c0c0c0c,之前的256字节的0x0a用于填充缓冲区。
图1.2漏洞利用的代码,只是简单地跳转到0x0c0c0c0c处执行,当然现在0x0c0c0c0c处还没有能够执行的代码。
在此基础上HeapSpray就可以实现漏洞稳定利用,很适合做演示。下面开始正题啦O(∩_∩)O~:
一、 如何控制堆块的分配
实验中将溢出后覆盖的返回地址设置成0x0c0c0c0c,所以0x0c0c0c0c处的数据就必须布置成空指令和ShellCode的组合,堆内存块的分配是不确定的,需要确定分配的堆块大小及分配次数才能保证正确覆盖要挑战到的地址,先以XpSp3+IE6下无DEP防护的利用代码为例(图2.1),看一下堆数据的布置:
图2.1:IE6下无DEP防护的HeapSpray漏洞利用代码
解释下关键代码的含义,牵扯的内容比较多:
这两句代码最终使nops所在堆块单元占用的空间为0x100000,可能不容易理解,第一句明明是0x100000/2再减了那么多变量,这就涉及到了javascript中BSTR字符串的内存空间占用情况(很多图片摘自资料一,资料一里有关堆块的分配原理部分讲的很详细,最好是先看下,动手试下附带的代码,熟悉Windbg使用,并加深对堆块分配的理解,再看下本文后续部分,在本文中尽量但不大可能将原理讲清楚,原谅俺吧╮(╯_╰)╭):
BSTR字符串首部占4个字节,包含字符串长度信息,后2个字节为截止符0x0000,中间部分是被转换成Unicode的字符串。再来看一下字符串所在堆块的结构:
堆块部分除了字符串所占的空间就只包含一个首部,XpSp3的堆块占32字节的大小(XpSp2之前的堆管理结构只占16字节,后来Sp2及之后的添加了很多检测堆溢出之类的结构,在这个地方纠结过),这样也就能够清楚这个图中的减去值的含义:
至于为什么都除以2,是和初定义nops的解码编码字符串函数unescape有关的,见var nops=unescape(“%u9090%u9090”),用unescape的.length去检测分配的内存块大小时实际大小是其返回值的两倍。所以最终nops字符串所在堆块占用的内存空间大小才恰恰是0x100000字节。了解了这些明白上面IE6下的漏洞利用代码(图2.1)就比较容易了:先在堆里分配200个大小为0x100000大小的内存块,一般0x0c0c0c0c处的代码就会被我们布置的空指令+ShellCode覆盖,下面触发漏洞跳转到0x0c0c0c0c处的空指令执行(如果刚好跳到ShellCode中间导致Shellcode被破坏的话算你倒霉,再来几次就好,当然这概率很小,和空指令相比ShellCode并不长),最终执行我们的ShellCode。
IE6下堆内存的分配就是这样,但是在IE8下,上面的堆喷射(HeapSpray)代码并不能覆盖0x0c0c0c0c处的区域,解决办法很简单,将下图红框部分代码:
替换为:(其实就替换了一句。。。)
这样喷射代码分配的堆内存块就能覆盖0x0c0c0c0c处的堆空间了。
二、 ROP链的布置
虽然我们已经能够控制堆内存的分配,绕过DEP还是有很多关键的步骤需要实现,如ROP链的获取及布置,主要难点在于堆块加入了ROP链之后如何分配,覆盖返回地址之后怎样才能刚好从ROP链的开始处执行,做完这一步,后面那些小问题就好说了。
ROP链的获取: 由于IE网页不能直接调试,可以通过在网页javascript代码中插入弹窗语 句达到暂停运行的效果之后再用调试器附加,到达需 慢慢跟踪的地方,比如需查 看堆块分配情况等,当IE运行时的模块都加载完毕之后,就可以使用工具immunitydbg的mona插件(一 个Python脚本,放置在immunity的PyCommands 目录下),输入命令:“!mona rop -m 模块名.dll”,就可以在DLL文件内存空间自 动搜索可用的跳转地址,稍候片刻就可以在安装目录下生成的rop_chain.txt 中查看各种语言格式的ROP链,当然mona的功能不止于 此,但是现在这条命令就足 够了,图1.2就是通过这种方式得到的javascript格式的ROP(还需稍微修改才能 够真正使用,后面会 提到)。
ROP链的布置:
为 了达到绕过DEP的效果,ROP就链必须要先于ShellCode执行,如果是栈溢出且栈中能够存放ROP链的地址的话就好办了,布置在栈中比不知道堆中 定位容易,将ROP的第一个地址覆盖溢出的返回地址,这样就能通过溢出的返回地址先执行ROP链地址处的指令,执行完毕之后再跳转到0x0c0c0c0c 之类的地址执行就可以达到目的,偏偏超星阅览器4.0虽然是栈溢出,但是对栈中布置的数据进行了转化,不仅不能简单地将ROP布置到栈中,仅有的覆盖栈中 返回地址的指令也要进行仔细选择,在演示过程中会说明选择指令的原因及对策,现在主要说如何将ROP链布置在堆中:
图3.1 ROP的布置,可看做一个ROP+Shellcode单元
将 ROP和紧挨着的ShellCode作为分配的堆块的一部分,ROP链地址处的指令执行完后就可以接着执行ShellCode,这里需要解决的问题是要 ROP链先执行(严格意义上不是这么说的,这里及以后都指的是地址处的指令),我们需要控制返回地址精确指向ROP的开头,因为堆块是 javascript语句随机分配的,所以需要ROP地址可控,来看一下现在的解决方案(图3.2):
图3.2 ROP的布局:达到0x0c0c0c0c总是指向ROP开头,ROP链相对堆块起始地址的偏移是固定的
之前的测试中,我们已经能够分配固定大小的堆块(例子是分配0x100000大小),当分配的堆块足够多的情况下,堆块的起始地址会变得固定,如图3.1所示,堆入口地址的后四位都是0018,对应的用户数据起始地址为0020, 这也不是必然的,只有大量分配堆块或是使用后面提到的HeapLib进行精准堆分配时才会有这种现象,在我的测试过程中,只在最初几次实验时看到比 0x0c0c0c0c低好多的堆内存空间堆块起始地址不规则的情况,但是0x0c0c0c0c所在堆块空间的起始地址的后四位总是0018。前四位的地址 可能会变化,比如0x0c0c0c0c这次是处在0c0a0018开始的堆块,下次处在0c0b0018开始的堆块,这种影响可以通过重复 ROP+ShellCode单元块(图3.1)消除,因为后四位0018一般是不变的,按照图3.2中ROP+ShellCode的布局,如果两个堆块间 的间距为0x10000,可以每个堆块内布置10个(举例而已,具体的个数可灵活,没什么大的影响)这种nops+ROP+ShellCode+nops 的单元,每个单元占0x800*2=0x1000字节,并且必须保证ROP前有(0x0c0c0c0c- xxxx0020)/2%0x1000=0x5f6个nop指令填充,这样就达到了无论0x0c0c0c0c所在堆块起始地址前4位的是多 少,0x0c0c0c0c总能够指向ROP链的开头的目的(不知俺说不说的清楚,0x0c0c0c0c堆块起始地址后四位是固定的,前四位无论怎么变化, 影响分配的只是0x0c0c0c0c离堆块首部地址XXXX0018(用户数据是从xxxx0020开始的)之间 nops+ROP+ShellCode+nops的单元的个数不同而已(单元大小0x1000字节,0c0b0018和0c0c0018分别为堆起始地址 距离0x0c0c0c0c分别有5个和0个单元)最终的分布结果总是类似于图3.2所示,当然单元大小和分配的多少都可以灵活制定)。
后 面的路基本上就是一片坦途了,既然0x0c0c0c0c已经能够成功指向ROP链的开头,下面就是控制溢出让ROP最先执行了,因为ROP是由一系列返回 地址组成的,第一条地址就必须成为返回地址之后才能继续执行,但是溢出的时候返回地址是返回到的栈上,并不能直接把返回地址覆盖成0x0c0c0c0c, 然后跳到ROP链,一是一直强调的ROP链不是指令,而是指令所在的地址,二是DEP还没有关闭,0x0c0c0c0c处的数据并不能当做指令执行,这时 还需要做的就是翻转堆栈(这个地方纠结了好久,其实解决方法很简单),让ESP寄存器赋值为0x0c0c0c0c,我们布置数据的默认堆就变相成了栈空 间,retn、push、pop等指令就可以想操作栈一样控制堆空间,等执行RETN操作时ROP链第一条地址被弹出到EIP寄存器,就齐活了,ROP链 地址对应的指令就会顺序执行完毕关闭掉DEP,怎么翻转堆栈呢?找两处地址足矣:先将栈顶数据设成0x0c0c0c0c,再从加载的DLL里找 pop eax retn、xchg esp eax retn指令的地址,分被记为retaddr1、retaddr2,控制返回地址先返回到 retaddr1执行pop eax,则eax=0x0c0c0c0c,retn之后再来一把retaddr2地址处的xchg esp eax,则 esp=0x0c0c0c0c,再retn就可以开始ROP链的地址处的指令了。
三、 对抗WinXpSp3+IE8
前面罗嗦了这么多,原理部分大致讲完了,开始对XpSp3+IE8动刀吧,还是推荐先动手试验下资料一的内容,最前面堆块分配的原理部分讲解的很详细,在实际利用过程中遇到问题的再看本文对应部分。
来看下我们最终的利用代码(图4.1):
图4.1最终的利用代码。
这里还有几个需要注意的地方:
1. Immunitydbg获取的ROP链很多情况下都有一些错误,必须了解ROP链的过程,对出现的错误进行修改才行,之前找的msvcrt.dll的ROP链,要改的有一处:
本来eax已经指向visualprotect的函数地址了(POP EAX),但是这里又加了EF,导致出错,解决的办法就是地址77be1120改成77be1131,最后AL寄存器再加0xEF的时候才变成77be1120。
2. 之前我们已经分析了IE6精确分配堆块大小的算式:
至于为什么IE8分配的时候变成了算式(变量定义的不同,主要看红框里的算式,end已经包含了ShellCode):
俺也不大清楚了,只能说是前辈们总结的经验。
附经验:
ROP的偏移有时也是不同的,经验图:
3. 注意3里标注的两个地址分别是pop eax;retn和xchg esp,eax;retn,注意看这两个地址的每一个字节\x1B\x71\x38\x63\x2d\x30\x3b\x63都是小于128的,因为字符串str经过了先转化为UNICODE再转化为ASCLL的操作,如果选取的地址中有大于128的字节,如\x90经过两次转化之后会变成乱码,导致原数据被破坏。Msvcrt.dll里没有适合的这种地址,选取其它DLL里的,类似XCHG ESP,EAX;RETN之类的地址可以使用OD的findaddr插件(能搜索常见的漏洞利用地址,但是不包括这个),搜索指令的机器码就可。
四、 移植到Win7+IE8
XpSp3的IE8搞定之后,再去搞Win7的IE8就轻松多了,看一下代码,也有几个移植时需要注意的地方,因为还需要处理Win7的ASLR(布局地址随机化),每次加载的DLL文件其基址在下次重启之后就会变化,导致我们ROP链的返回地址对应的指令也会变化,一个解决办法就是在未开启ASLR保护的DLL文件中找返回地址,一般的资料里都利用的安装java6之后IE加载的msvcr71.dll,其实安装完超星4的ACTIVEX控件之后,发生溢出的pdg2.dll也不支持ASLR保护,完全可以在pdg2.dll里找需要的返回地址构造ROP链,但是,嘿嘿嘿,从pdg2.dll里找到的ROP链也不能直接用,还在调试中,如果你能看到这几句话说明我还没有调试修改成功。就先从msvcr71.dll找需要的ROP吧,看代码:
解释下实际使用需要注意的几个地方:
1. 这个地方已经在XpSp3的地方讲过了,要改的地方相同。
2. 这里还是经验,XpSp3那里已经把各操作系统各IE版本堆块喷射的参数列出来了,只有before分配的那个0x5F4需要调试时再确定一下。
3. msvcr71.dll里并没有找到XCHG ESP,EAX;RETN指令的机器码,这地址是在pdg2.dll里找的,同样每一字节的值不能大于128(ASCLL能表示的范围,再转化回来不会出错)。
五、 漏洞稳定利用->精准堆分配
前面提到了堆块的分配是随机的,起始地址可能会变化,有一定的概率导致0x0c0c0c0c不能准确的指向ROP链的开头,导致利用失败,为了精确控制堆的分配,就要用到Alexander Sotirov提出的堆风水(Heap Feng Shui)技术, heaplib javascript library用于实现,很容易使用其提供的heaplib javascript链接库来实现精准堆分配,也可以不去了解原理,掌握使用方法就行,而且就算是不使用堆精准喷射也能很好的稳定分配堆内存。
heaplib链接库使用比较简单,先将定义及实现的放置在漏洞利用代码前面(只显示部分,全部代码见附件里的最终利用代码里):
分配堆块也比较简单:
先调用heap_obj.gc清除堆里分配的零碎堆块,再使用heap_obj.alloc函数分配所需空间即可。
看下效果:
分配的堆块:
六、 工具及资料
工具:
Ollydbg(偏爱的)、Windbg(查看堆内存分配等)、immunitydbg(与Ollydbg类似,支持Python脚本,Mona插件需另外下载,会打包进资料里,可用于自动查找ROP链)。
http://pan.baidu.com/share/link?shareid=2622913232&uk=554923767
参考资料:
exploit编写系列教程第十一篇(经典详细)
实战HeapSpray之CVE2012-1889 Exploit编写
www.programlife.net/heapspray-cve2012-1889-exploit-1.html
[译]各种IE版本下的堆喷射技术
http://www.programlife.net/heap-spraying-in-internet-explorer.html