正好在Google搜到了这篇文章,就打算自己翻译一下,也不清楚国内是否有人已经翻译过了。作者是Pwn2Own 2010的获奖者来自荷兰的皮特·维莱格登希尔(Peter Vreugdenhil)。 2010的Pwn2Own是第一次公开亮相的攻破有ASLR+DEP保护的IE浏览器,所以也是我比较关注的一点。当然了就目前(2016)来说,这些都已经是漏洞利用的标配了。所以是一次考古性质的翻译。
ps:我是根据对技术的理解去翻译的,很多地方都有删改,要求精准的可以去看原文。
http://vreugdenhilresearch.nl/Pwn2Own-2010-Windows7-InternetExplorer8.pdf
我决定写一篇关于我在Windows7下攻破有DEP和ASLR保护的IE8浏览器的技术文章。
整个利用过程分为两大步骤
第一步是找出一个确定的dll文件的加载基地址,然后在第二步中会使用到第一步中得到信息来使用一些ret2libc技术去bypass DEP,然后劫持流程去执行shellcode。我暂时还不能公开详细的漏洞细节,但是等微软修补过后我会逐步公开细节的。就像我前面说的那样,我使用了两个exp才实现最后的任意代码执行的结果。
在第一步中,为了获得ie浏览器中一个dll模块加载的基地址,我使用了一个堆溢出覆写了一个UTF8 String(对象?)的尾部结束符(x00x00),这样当我读这个字符串的时候就可以读到下一个对象的虚表指针了。当然了,下一个对象也是我精心布置好的。实现这个想法花了我好几天的时间,最终才实现了稳定的利用。
我画一个堆内存的示意图,
黄色的内存是发生溢出的堆块。
绿色的内存是我们分配的Unicode String。其中结尾符x00x00由亮绿色表示。
红色的内存是我们精心分配的对象,其中亮红色表示对象的虚表指针。
虽然看着好像很简单,但实际实现却有很多困难。这需要一些技巧才能把内存布局成我们想要的形式。首要的前提是浏览器要存在着一个可以控制的堆溢出,并且我们的这些操作不能让浏览器崩溃掉。然而,经过了一番努力之后,我成功的构造了想要的布局,而且可靠性很高。然后我触发了堆溢出,覆盖了字符串结尾的字符。这样字符串就不再以x00x00作为结尾了。如果我们接下来用js函数去读取这个字符串的内容的话,那么就会一直读到下一个x00x00为止。这样js函数就读到了下一个对象的虚表地址。虚表地址有什么用呢?虚表在编译时就已经编译到了dll模块中,就是说虚表的地址到模块起始的偏移是确定的。所以根据虚表地址就可以计算出来对象所处的dll模块的基地址。这个信息可以用来编写一个绕过DEP的exp(实质上是bypass ASLR)。如果我对写入溢出的数据拥有完全控制的话,覆写下一对象的虚表就很容易了。但是如果我对溢出数据的控制受到限制的话,我就不能确保可以覆写虚表指针了。
第二步就是想办法绕过DEP的保护了,几个月前,我写了一个绕过IE8下DEP保护的exp,使用的技术是堆喷射+ROP。事实上是混合使用了堆喷射和对象调用伪造,但是我并不确定我是不是第一个使用这种技术的人。在我调试一个IE浏览器的UAF漏洞时,我注意到大堆块的分配是可以预测的。但是不是精准的预测,而是我发现分配地址的最后两个字节总是相同的。在XP系统上大于100字节就会是可预测的。这样一来我们就足够去做堆喷射,并且很有可能猜到喷射到的地址。
举一个例子,下面的代码将不断的分配一些填充有固定内容的堆。这个堆的分配地址应该是开始于0xZZZZZY20的,其中Z的值是随机的,Y则只能为0/4/8/C。因为我分配的单元大小是0x200个字节。
1 heap = new heapLib.ie(0x20000); 2 var heapspray = unescape("%u4141%u4242"); 3 while(heapspray.length < 0x200) heapspray += unescape("%u4444"); 4 var heapblock = heapspray; 5 while(heapblock.length < 0x40000) heapblock += heapblock; 6 finalspray = heapblock.substring(2, 0x40000 - 0x21); 7 for(var i = 0; i < 500; i++) 8 { 9 heap.alloc(finalspray); 10 }
这段代码的执行结果可能是这个样子的
Heap alloc size(0x7ffc0) allocated at 063d0020
Heap alloc size(0x7ffc0) allocated at 06450020
Heap alloc size(0x7ffc0) allocated at 064d0020
Heap alloc size(0x7ffc0) allocated at 06550020
Heap alloc size(0x7ffc0) allocated at 065d0020
Heap alloc size(0x7ffc0) allocated at 06650020
Heap alloc size(0x7ffc0) allocated at 066d0020
如上可见,你得到了堆中彼此相邻的块。
上面例子中的分配大小对于你来说是没有用的,因为这个大小取决于你的具体漏洞情况。
就像代码中写的那样,我使用heaplib来分配字符串,这样更加的方便。
这种技术在Windows7上依然可以使用, 但是分配的次数要比在XP上更多一些。比如在XP上以500为堆喷的次数,0x0a042020作为起始地址。那么到了Windows7上则要进行900次喷射,并且以0x16402020作为起始地址。这样的结果是只要喷射的次数足够多的话,那么我们就能预测出堆分配的地址(而且这个地址可以我们来指定)。
下面来讲解一下简单的UAF漏洞如何进行利用。我发现的UAF漏洞通常都是这样子的:发生UAF的对象在不同行的js中被分配和释放,所以我们有充足的时间去我们指定的数据占位被释放的对象内存空间。
这是我们假设的存在UAF漏洞的代码:
var MyObject = new Object();//分配对象 var MyObjRef = MyObject.SomeProperty; //引用对象 MyObject.CleanUp(); //释放对象 alert(MyObjectRef.parent);//解引用对象
现在我们希望在对象被解引用之前,用我们控制的数据对释放的对象内存地址进行占位。在大多数的UAF漏洞中,发生UAF对象的虚函数都会发生调用。虚表指针就是一个对象的前四个字节的值,所以我们需要进行某种类型的堆喷射可以准确的填充到我们在对象虚表值覆盖的值的位置。
一个好消息是IE对刚刚释放的堆块进行了记录,如果我们申请一个大小大约相同的堆,那么那块内存就会被重新分配(我很怀疑真的是大约相同吗?)。这意味着,如果我们知道释放对象的内存大小,我们就可以用相同大小的我们自定义的数据去分配堆,而且结果会是相同的内存。
想知道对象的大小并不能,只需要对ntdll中的堆分配函数下断即可。我们必须再次分配正确的内存大小,一般我都是通过对div标签数组添加className属性来实现的这一点。这么做的优点是className属性是一个字符串,你可以对这个字符串指定任意大小。这个过程不会导致额外的堆分配(额外的堆分配可能会导致不能正确占位),并且字符串的内容是你自定义的,你可以指定第一个DWORD大小的数据为任意值。唯一的缺点是不能在字符串中使用x00x00,但是事实上占位根本用不到x00x00。
所以我们需要做一下几件事
- 创建数组来存放div元素( var DivArray = new Array(); )
- 用50个对象来填充这个数组
- 当UAF对象被释放掉后,执行js语句对内存进行占位
- 给divs标签添加classname属性( DivArray*i+.className = unescape(“%u4141%u4141......
- 解引用UAF对象
这样操作之后的结果会怎么样呢?
最可能的结果是这个样子的:
move eax, [ecx] ecx = our object memory.
call [eax+0x34] eax now holds 0x41414141
0x6ff02348 :
mov ecx,eax
call [eax+10]
and another that goes like:
0x6FF01234 :
push [eax+70]
push [eax+60]
push [eax+50]
push [eax+40]
push [eax+34]
push [eax+20]
call [ecx+14]
.....
.....
retn
那么,如果我们这么去设置堆喷的堆内容的话:(根据引用顺序排序)
- +0x34:0x6FF02348
- +0x10:0x6FF01234
- +0x14:0x7c801ad4 (VirtualProtect)
- +0x20:堆喷的首地址
- +0x30:0x200(单个堆喷块的大小)
- +0x40:0x40(READWRITEEXCUTE)
- +0x50:无意义的值,占位用
这样的话我们就成功的调用了VirtualProtect函数把内存的属性改成了可执行的。这种方法在XP上能够得到很好的执行,因为XP系统上我们可以准确的知道VirtualProtect的地址。在Windows7上利用我们需要更多的创造性。
我们在不知道kernel32.dll模块的准确基地址的情况下该如何调用VirtualProtect呢?IE的许多dll模块包含有ATL库,这些ATL库中会调用VirtualProtect函数。这意味着VirtualProtect函数处于我们已知的一个固定的偏移(相对dll基址)。假如我们知道dll加载的基地址是0x6fff0000,0x6fff1288是VirtualProtect函数的地址。我们需要做的就是在诸如call [eax+8]这样语句之前把eax设置为0x6fff1280。这可以通过想上面那样利用代码并设置堆喷的内容来实现。当我使用这种方法时,我经常把我的堆喷内容设置为连续增长的值,比如:
var pattern = unescape(“%u0000%u0001%u0002%u0003%u0004%u0005%u0006........”)
通过这种方法,就可以更容易的找出你的字符串中需要编辑的值。
本质上,我们要做的全部事情就是连接起已经准备好的这些代码块(gadgets)。但是只能使用call或是jmp去连接,直到成功的执行了VirtualProtect。然后我们的堆喷就具有了可执行权限。如果我们的执行流中向栈中压入的参数多于VirtualProtect函数需要的,那么返回堆栈将会被破坏,并且会结束掉我们的流程。
Peter Vreugdenhil
翻译 by:Ox9A82