-
-
[翻译]Windows exploit开发系列教程第九部分:堆喷射[第二章:UAF]—大海捞针(Finding a needle in a Haystack)
-
发表于: 2018-3-15 20:34 4681
-
[翻译]Windows exploit开发系列教程第九部分:堆喷射[第二章:UAF]—大海捞针(Finding a needle in a Haystack)
fuzzySecurity于16年更新了数篇Windows exp教程,这一章节主要是继承第八部分的堆喷射,继续围绕UAF而展开。点击查看原文。
堆喷射的前一部分在很久之前蛙师傅已经翻译过了,附上链接于此。
欢迎回到堆喷射系列两部分教程的第二部分。本文会在IE8上完成精准的堆喷射。在这两种基本情形下你需要实现严格而精准的堆喷射:
我想找出一个案例来同时处理这两种情况,让你痛并快乐着。然而大部分这种漏洞本身都相当复杂,不适合也不需要作为初学者的导读内容。这里需要澄清两件事。首先就是熟能生巧,找到漏洞,将他们拆分,疯狂打脸。然后加把劲,少挨几个耳光并继续前行。其次本教程并不会聚焦于漏洞的分析,本教程旨在描摹你在编写exp是会遇到的障碍以及教会你如何去克服它们。
今天我们来看看MS13-009, 你可以在这里找到对应的metasploit模块here。我也附加了一些阅读材料的链接在下面,我强烈推荐你阅读以下,如果你想要对这个课题更加了解的话。
Debugging Machine:
Windows XP SP3 with IE8
Links:
Exploit writing tutorial part 11 : Heap Spraying Demystified (corelan) - here
Heap Feng Shui in JavaScript (Alexander Sotirov) - here
Post-mortem Analysis of a Use-After-Free Vulnerability (Exploit-Monday) - here
Heap spraying in Internet Explorer with rop nops (GreyHatHacker) - here
CVE-2013-0025 MS13-009 IE SLayouRun (Chinese analysis of ms13-009, you will probably need to load this from the google cache) - here
我想这个话题需要一些介绍,否则你会发现我们即将遇到的大部分障碍都不怎么熟悉。我不会非常详细的展开,因为那太过于耗时了。如果你对这里的一些话题不是很熟悉的话,我建议你先阅读本系列的第7部分(ROP)和第8部分(堆喷射[第一章:Vanilla EIP])以及上面的阅读材料。
在我们讨论UAF之前我们需要先理解虚表(vtable)是什么。C++语言允许基类去定义虚函数。而对于子类来说,它们可以重新去定义这些虚函数的实现体留为己用。也就是说虚函数允许子类去替换基类所提供的实现体。编译器确保被替换的函数永远会被调用,只要该对象实际上是一个子类的话。这些都发生在运行时。
译者注:这里实际上说的不是太理想,虚函数就是子类可以重新定义实现体,且会取代父类提供的实现体。C++用此实现多态,即运行时动态绑定。而所谓多态,简单理解就是——当用父类指针指向子类对象时,一旦调用到虚函数,最后执行的会是子类定义的那个实现体,即作者所说的那个replacement。
虚表保存了基类中定义的各种虚函数实现体的指针。当一个函数在运行时需要被调用时,合适的指针会从对应的子类虚表中选择出来。我们可以看这样一张表示图。
UAF漏洞往往较为复杂,引发的问题也变化多端。通常执行流会像这样:
想要利用这个议题我们将通常执行这些操作:
通常这些听起来贼复杂,但是随着我们示例的展开,一切都会渐渐清晰。首先我们创建一个可信的堆喷射,此后我们再聚焦于MS13-009!
如在第八部分中做的那样,我想先以IE8的一个可信堆喷射开始。继续此前的工作,我们使用下面的POC。该POC有些轻量改动。最主要的不同在于我增加了一个alloc函数,它会将我们输入的buffer尺寸进行调整,以便于它们可以匹配BSTR的规格(我们需要扣除6以补偿BSTR header和footer,除2是因为我们用的是unicode unescape)。
快速看看调试器,堆喷射都发生了什么。
下图是我们堆喷射的可视化显示。我们用150mb大小的数据填充了堆,这150mb被分成了150个1mb的块(每个块都以独立的BSTR对象存储)。该BSTR对象由0x1000(4096字节)的块填充,它包含了我们的shellcode和NOP指令。
到目前一切顺利!下面我们需要重组我们的堆喷射,以便于shellcode指针精准的放在0x0c0c0c0c,这里将是我们的ROP链首。考虑到由于我们的堆喷射,如果0x0c0c0c0c在内存的某个地方被分配了,那么它在0x1000块的内部一定有一个特定的偏移。我们想要做的就是计算出0x0c0c0c0c到块首的偏移,并把这个偏移作为喷射的padding。
如果你重新运行上面的堆喷射,你会注意到0x0c0c0c0c并不总是指向同一个堆项,然而0x1000块到0x0c0c0c0c的偏移却始终是不变的。我们已经掌握了padding尺寸计算的所有信息。
让我们修改POC并重新运行堆喷射。
我们可以看到我们已经重组了shellcode到0x0c0c0c0c。实际上当我们在内存中搜索字符串"FuzzySecurity"时我们可以看到所有的位置都以相同的字节序列"0x?????c0c"终止。
我们现在重组了堆喷射,且可以把shellcode放到选择的任意地址(本例中是0x0c0c0c0c)。堆喷射在Windows XP/7上的IE7-8上运作良好。稍微修改一下也可以在IE9上运行,但这已经超出了本教程的范围。
译者注:IE9引入了Nozzle,但实际上非常好绕过,Nozzle不允许毗邻堆块完全一致,绕过方法也很简单,每个chunk的junk值都不同即可(实际上修改1个字节即可)。另一方面,到了DEP的时代,0x0c0c0c0c这个值已经丧失了原本的意义,原本用于sled的0C指令也就无需执行,改成什么其他的数都是可以的,这也要求我们的堆喷需要十分精准的到ROP链首,exactly。
如上面所提到的,本文主要的目的不是去分析漏洞,而是理解在编写exp时所遇到的障碍。我们会快速的看一下这个漏洞以理解发生了些什么。下面给出触发该bug的POC。
好的,让我们在调试器中看看触发漏洞时发生了什么。你会注意到我添加了CollectGarbage()函数(但注释掉了)。在我的测试中我注意到这个bug有一点不稳定(只有80%左右),因此我曾用CollectGarbage()来实验看看他是否会提升稳定性。CollectGarbage()是个javascript可见的函数,它会清空四个bin,这4个bin用于在oleaut32.dll中实现一个自定义堆(custom heap)管理引擎。当我们试图去在堆上分配自己伪造的对象时,它会变得非常有用。在我的测试中,我无法判断它是否会引起差异,如果有人直到的话请给我留言。
从下面的执行流来看,有一个试图去调用虚表中函数的对象,调用位置在eax偏移0x70处。
栈回溯显示了执行流,最终引导至崩溃。在调用期望返回处程序未崩溃之前,如果我们反汇编(unassemble)该返回地址,就可以看到该函数是如何被调用的了。看起来某个在EBX的函数传递了它的虚表指针给ECX,然后由mshtml!CElement::Doc所引用来调用一个在偏移0x70处的函数。
通过设置一些巧妙的断点,我们可以追溯到mshtml!CTreeNode的分配来看看是否有熟悉的值出现。下面的结果显示EBX指向CParaElement,被调用的函数是CElement:SecurityContext。这看起来和MS13-009的漏洞描述一致:“IE浏览器的一个UAF漏洞,CParaElement节点被释放了但在CDoc中却仍旧保留了一个引用。当CDoc重新布局时这段内存会被重用到”。
如前面所提及,这里的主要焦点是如何克服编写exp过程中的各种阻碍,因此我不花时间来解释如何在堆上分配我们自己的对象。取而代之的是,我会使用一个公共可用的exp的片段。我们的新POC如下所示。
再次注意到CollectGarbage()函数,用它来看看在分配对象内存时是否有显著的区别。让我们看看执行POC后,调试器中发生了什么。
这一串指令以调用0x0c0c0c7c处的DWORD值(=EIP)而结束,如果0x0c0c0c7c在内存中是一个有效的地址的话。记住我们的堆喷射将shellcode对齐到了0x0c0c0c0c,下面我们会看到为什么这是必要的。只要记住我们可以设置EIP为任何想要的值,例如0xaaaaaaaa里的DWORD。这可以由覆盖EAX为0xaaaaaaaa-0x70=0xaaaaaa3a来实现。你将在下面看到这个例子。
让我们在调试器中证实我们会以覆盖EIP为[0xaaaaaaaa]而告终。
走得挺远了!把这些内容放在一起我们就可以开启代码执行之旅了。首先创建我们新的POC,它包含了堆喷射以及触发漏洞的代码。
从下面的截图可以看到,我们覆盖了EIP为0x90909090,这是因为EIP从0x0c0c0c0c+0x70=0x0c0c0c7c中获取了DWORD值,它指向我们的nopslide。
这一开始会有点困惑,下面的图示会帮助你理清!
让我们尝试把shellcode进行padding从而让它精准的覆盖EIP。我们可以通过预置一个缓冲区长度为0x70的unescape ASCII字符串来实现(112-bytes = 28-DWORD's)。
如期所致,我们完美的控制了EIP。提醒一下,EIP的值是小端字节序。
0x1000块的新布局如下。
完美!现在我们不得不面对下一个障碍。我们的ROP链和shellcode会放在堆上,但是我们的栈指针(=ESP)指向了mshtml的某处。任何ROP gadget执行后都将返回到下一个栈上的地址因此我们需要劫持栈(Stack Pivot),把栈从mshtml的某处劫持到我们控制的堆上来(0x1000字节块)。你可能记得EAX指向了shellcode的起始,因此如果我们找到了一个ROP gadget可以将EAX交换给ESP的话(mov esp,eax/xchg eax,esp),我们就可以实现栈劫持并从0x0c0c0c0c地址处开始执行ROP链了。
我将从MSVCR71.dll中使用ROP gadgets,它是java6的包,会被IE自动加载。我通过 mona生成了两个文本文件:
如果你想要玩玩的话我建议你去下载这些文件并用正则来解析它们。
MSVCR71_rop_suggestions.txt - here
MSVCR71_rop.txt - here
通过解析文本文件,我们可以简单的搜索到想要的gadget,让我们修改POC,验证一下是否已然万事俱备。
译者注:stack pivot是个偷梁换柱的技术,把堆变成栈,然后在堆上放置的ROP链就发挥作用了。但利用后是否要进行复原操作,或者遗留副作用,则要具体情况具体分析了。另外,作者之所以仅用MSVCR71.dll来生成ROP,是因为这个dll没有开启ASLR,这就为ROP链的地址硬编码提供了稳定性。后期的IE可能找不到没有开启ASLR的模块,通过利用未开启ASLR的模块来绕过ASLR已经不适用了,但这并不代表无计可施,最常见的,我们可以找到某个可以leak info的地方,比如leak处某个对象的虚表地址,利用虚表地址在模块中的偏移就可以获取该模块的基地址(ASLR变的是基地址,但虚表的相对偏移不会变),此时就可以利用这个模块的ROP gadgets了。
从截图中可以看到命中了xchg eax, esp指令,如果我们继续执行的话,就可以成功的劫持栈并执行0x0c0c0c0c的第一个DWORD了。
目前已基本搞定。我们现在需要执行ROP链去禁止一个内存区域的DEP保护,此后我们就可以执行第二个舞台上防止的payload了。幸运的是,MSVCR71.dll已经被攻击者反复滥用过,该dll有一个优化过的ROP链可用,它由 corelanc0d3r here创建。让我们把这个ROP链插入到POC中并重新运行exp。
从截图中我们可以看到我们在栈劫持后成功命中了第一个gadget,在调用VirtualProtect后直达我们留下来的junk处。
现在剩下的就是在junk buffer的最后插入一个短跳转,跳过我们的初始化EIP覆盖位置(XCHG EAX,ESP #RETN)。这里可以放任何shellcode,在短跳转后就得以执行!
执行短跳转后,我们跳到了覆盖EIP的位置正后方,我们现在就可以执行任何选择的shellcode了。
最简单的部分,让我们生成一些shellcode!
很好,现在我们清理POC,增加注释,运行最后的exp。我想再次提及的是这个漏洞有一些不稳定性(大概80%复现),如果有人有什么好主意请留言。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [翻译]Windows 10 Segment Heap内部机理 19880
- [翻译]Windows 8堆内部机理 7159
- [翻译]深入理解LFH 7787
- [翻译]Bitmap轶事:Windows 10纪念版后的GDI对象泄露 9296
- [翻译]理解池污染三部曲 6821