前言

这篇文章讲述的技术是基于泄漏栈地址并覆盖结构化异常处理程序,将use-after-free转换为结构化异常处理程序覆盖,从而绕过CFG

我选择使用Internet Explorer 11的漏洞来帮助此次绕过,该漏洞在2016年6月MS16-063中补上了。

泄漏栈

我附上了Clean_Poc.html POC文件,利用该漏洞获得了读写原语。在下一个名为Leaking_Stack.html的POC文件中,当前线程的栈限制被泄露,这是通过使用kernelbase.dll中的GetCurrentThreadStackLimits API实现的。它的执行方式是通过虚拟函数表(vtable)覆盖TypedArray对象,使用下面列出的调用:

绕过CFG-结构化异常处理-RadeBit瑞安全

它在vtable中的offset 0×188处,可以直接从Javascript代码调用,而且有两个参数。这很重要,因为函数必须使用相同数量的参数,否则栈将在返回时不平衡, 这会触发异常。

我遇到的API是GetCurrentThreadStackLimits,它满足JavaScript的调用要求,在MSDN中它如下所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

它有两个参数,并返回栈基址和栈的最大保留地址。GetCurrentThreadStackLimits的地址可以通过两个步骤找到,首先泄漏一个kernelbase.dll指针,然后在DLL中定位该函数。第一部分是通过定位jscript9中的Segment :: Initialize函数来完成的,因为它使用了kernel32!VirtualAllocStub,继而使用了kernelbase!VirtualAlloc。我通过从vtable的地址扫描jscript9并计算哈希发现了此函数,这是通过使用读取原语实现的。算法如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

这个哈希是如何找到的呢?方法是通过添加5个DWORD,并每次向前一个字节,直到找到正确的哈希。这个非常简单的哈希函数实际上是无碰撞的。对kernel32!VirtualAlloc的解引用调用是在Segment :: Initialize函数中的offset 0×37处,如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

读取这个指针,得到如下信息:

绕过CFG-结构化异常处理-RadeBit瑞安全

然后它在offset 0×6处包含解引用跳转到 kernelbase!VirtualAlloc:

绕过CFG-结构化异常处理-RadeBit瑞安全

现在我们有一个指向kernelbase.dll的指针,然后我们使用与Segment ::Initialize相同的方法找到GetCurrentThreadStackLimits的地址,如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

我们现在可以创建一个假的vtable,并在offset 0×188处用这个函数指针覆盖vtable entry,同时记住增加TypedArray的参数大小,代码如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

在运行它时打断GetCurrentThreadStackLimits得到以下内容:

绕过CFG-结构化异常处理-RadeBit瑞安全

图中显示了栈的下限和上限。为了从这里获取指令指针控制,我在栈中找到结构化异常处理程序链,并覆盖一个entry,然后触发异常。在执行此操作时,重要的是要记住Windows 10启用了SEHOP。这将绕过CFG和RFG,因为SEH指针不受CFG的保护。这一切都在文件Getting_Control.html中实现。

为了执行此操作,我需要在栈上找到SEH链,泄漏栈限制后,结构化异常处理程序链如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

在调试异常时,如果问题不解决,那么指向jscript9的5个结构化异常处理程序指针都将被使用,而MSHTML!_except_handler4似乎永远在异常中循环。因此,如果我们可以覆盖5个JavaScript异常处理程序中的任何一个,并触发一个异常,我们将获得指令指针控制。在一个老式的结构化异常处理程序覆盖利用中,整个SEH链被栈缓冲区溢出所覆盖,但这将触发SEHOP,所以我们只想覆盖其中一个异常处理程序的SEH记录,同时保持NSEH的完整性。因此,覆盖必须精确,SEH记录的栈地址必须泄漏。要执行此泄漏,我们将扫描栈并寻找SEH链,并确定我们已经找到它,我们可以验证最终的异常处理程序是ntdll!FinalExceptionHandlerPadXX。由于最终异常处理函数在应用程序重新启动时发生更改,因此泄漏分两步执行,首先找到正确的最终异常处理函数,然后找到SEH链。为了获得第一个泄漏,栈搜索ntdll!_except_handler4,因为我们只有在从上向下搜索栈时才会在栈上遇到它,如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

剩下的问题是找到ntdll!_except_handler4的地址,这很容易,因为可以从任何受CFG保护并包含一个间接调用的函数中找到ntdll.dll的指针。CFG验证包含对ntdll!LdrpValidateUserCallTarget的调用,并且由于jscript9.dll受CFG保护,任何间接调用的函数都包含直接指向ntdll.dll的指针。一个这样的函数位于TypedArray对象的vtable中的offset 0×10处:

绕过CFG-结构化异常处理-RadeBit瑞安全

使用读取原语,可以通过以下函数找到指向ntdll.dll的指针:

绕过CFG-结构化异常处理-RadeBit瑞安全

可以通过使用读取原语搜索特征或哈希来从ntdll.dll指针中找到_except_handler4的地址。_except_handler4如下图所示:

绕过CFG-结构化异常处理-RadeBit瑞安全

最前面的0×10个字节始终保持不变,并且非常独特,因此它们可以被用作无碰撞的哈希:

绕过CFG-结构化异常处理-RadeBit瑞安全

该函数将指向ntdll.dll的指针作为参数。一旦我们有了函数指针,我们可以搜索栈:

绕过CFG-结构化异常处理-RadeBit瑞安全

地址,我们有:

绕过CFG-结构化异常处理-RadeBit瑞安全

这意味着之前的DWORD可能会被读取,并且包含:

绕过CFG-结构化异常处理-RadeBit瑞安全

其中显示了最终的异常处理函数指针。接下来是第二阶段的泄漏,这次我们寻找的异常处理程序来自于jscript9.dll,所以它的函数指针一定位于PE的代码段内,这些地址是从DLL 的PE header中找到的:

绕过CFG-结构化异常处理-RadeBit瑞安全

现在从上到下搜索栈,查看栈的所有内容。算法的工作原理如下所示:

- 如果一个DWORD低于0×10000000,那么它不在jscript9.dll内,因此我们转到下一个DWORD。

- 如果它高于0×10000000,检查它是否在jscript9.dll的代码段内。

- 如果在,则栈上的DWORD 4字节是指向栈的指针。

- 如果上述两步是正确的,这可能就是我们正在寻找的结构化异常处理程序,因此我们跟随栈指针,并检查是否是以最终的异常处理程序结尾。

- 如果其中一个指针不再指向栈,或者需要超过8个解引用,那么它不是SEH链。

在我的测试中,第一次指向jscript9.dll的指针,其中的DWORD是一个栈指针,它是SEH链,所以算法运行速度很快。

绕过CFG-结构化异常处理-RadeBit瑞安全

有了这个算法意味着可以精确地覆盖结构化异常处理程序记录,并且不会中断下一个异常处理程序指针,从而绕过SEHOP。

最后要做的就是触发异常,从而获得指令指针控制:

绕过CFG-结构化异常处理-RadeBit瑞安全

运行结果如下:

绕过CFG-结构化异常处理-RadeBit瑞安全

上图显示了被调试器捕获的异常,然后指令指针被我们想要的0×42424242覆盖。这阐明了该技术是如何绕过CFG获得执行控制的。它也可以绕过Return Flow Guard实现, 这个在Creators更新中已不再支持。

代码在GitHub上,地址是:https://github.com/MortenSchenk/Bypassing_CFG_SEH