前言

本文来源于我在2016年7月的研究结论,由于各种原因现在才能发布。2016年6月,Theori曾发表了一篇关于MS16-063中修补了的IE漏洞分析,文中发布的exploit则仅是针对Windows7上的IE 11版本,此外由于Windows 10采用了CFG机制所以无法对Windows 10机器进行利用。

而本文就描述了我是如何在Windows 10下绕过CFG并进行利用的。事实上我还发现了另一种方法,会在接下来的一篇文章中提到。

了解CFG

控制流保护(Control Flow Guard,CFG)是微软在Windows 8.1 update 3和Windows 10下实现的一个保护机制,用以保护在汇编层下的直接调用。趋势科技分享的一篇Windows 10如何实现CFG的分析文章很不错。虽说目前已有数种公开的CFG绕过方法,但是这些方法大多是针对CFG的实现算法,而我想从功能的薄弱点入手。

Theori在其博文中讲到由于CFG机制,无法成功在Windows 7上成功利用,接下来就让我们来细究一二,然后尝试绕过它。

Theori分享的利用代码在Windows 10的IE下执行,直到调用虚函数表后被重写。所以剩下的问题便是,我们如何利用任意读写来绕过CFG。

根据趋势科技的研究,函数LdrValidateUserCallTarget调用CFG验证函数是否有效使用了间接调用,如下图所示:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

加载到EDX中的指针是验证bitmap的基本指针,在本例为:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

之后经过验证的函数将其地址加载到ECX,以kernel32!VirtualProtectStub作为例子,那么地址则为:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

接着地址向右偏移8位,用以加载存储着验证位(validation bit)的DWORD值:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

函数地址右移3位,然后执行位测试,实质上对偏移地址进行0×20模操作。然后利用验证的bitmap对DWORD进行检测:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

所以相关的位都在偏移地址0×14:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

这也就是说它是有效的,VirtualProtect存在有效调用地址,然而它的参数也必须由攻击者提供,所以我们还得再接再厉。通常情况是在ROP链中完成,但所有不是从函数开始的字节都是无效的,因此解决方案是找到一个在被调用参数是可以控制的函数,且函数的功能可以给攻击者提供便利。

在Windows 10中利用

Theori提供的exploit,代码执行是以stack pivot重写TypedArray的虚函数表。由于排除掉其他可能性,细究TypedArray提供的函数还是很不错的,以下两个函数就比较有趣:

如何绕过Windows 10的CFG机制-RadeBit瑞安全
如何绕过Windows 10的CFG机制-RadeBit瑞安全

偏移地址分别为0x7c和0×188,他们能从javascript代码中直接调用,而且HasItem有个可以控制的参数,与此同时Subarray存在两个用户可控制的参数,然而问题是它们都不返回除布尔值之外的任何数据。之后的问题便是,我们该选择哪个函数来进行覆盖。

此外选择的函数必须提供相同数量的参数,否则在返回时会导致堆栈不平衡而引发异常。我要找的API应该是可以向堆栈加载一个指针用以覆盖返回地址,从而绕过CFG。

我找到的API为RtlCaptureContext,在kernel32.dll、kernelbase.dll和ntdll.dll中都有调用,这个API有一个指向CONTEXT结构的参数:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

CONTEXT结构保存了转储的包含ESP的所有寄存器,此外输入值仅仅为一个可容纳数据的指向缓冲区的指针。如下为TypedArray对象的布局:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

第一个DWORD值是虚函数表指针,其能够被覆盖偏移地址0x7c处的API RtlCaptureContext的地址,然后创建一个假的虚函数表。同时偏移地址0×20下的DWORD是TypedArray用以指向实际数据的指针:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

由于它依旧可能泄漏缓冲区地址,我们就将其充当为RtlCaptureContext的参数。为了构造假的虚函数表,必须在偏移地址0x7c处创建一个指针指向ntdll!RtlCaptureContext,这意味着泄漏了RtlCaptureContext地址,也即ntdll.dll的地址也泄漏了。执行的默认路径需要使用一个指向jscript9.dll的指针的虚表地址:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

从该指针向上迭代0×1000个字节搜索MZ头,之后查找到指向kernelbase.dll的导入表。然后使用同样的方法找到kernelbase.dll的基址,接着找到ntdll.dll的导入表指针并再次获得其基址,最后从导出函数中找到RtlCaptureContext。

尽管这个方法是有效的但是有个缺陷,如果系统中装了EMET,会触发来自jscript9.dll的代码崩溃,因为从PE头或导出表读取数据是不被允许的,为了绕过EMET我使用了另外的技术。

记住CFG会保护所有的间接调用,由于jscript9.dll的函数被CFG保护了,所以不能调用直接指向ntdll的函数。偏移地址0×10下就有一个这样的函数:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

使用读操作,能通过以下函数找到指向ntdll.dll的指针:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

通过一个指向ntdll.dll的指针得到RtlCaptureContext的地址,这一步可以不通过查找导出表而是通过搜索签名或者hash完成。RtlCaptureContext内容如下:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

前0×30字节保持不变且看来相当的独特。如下所示当把他们整合在一起时,将其作为不受限制的哈希碰撞:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

函数可以使用指向ntdll.dll的指针作为参数。

接着将其全都整合到一起:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

缓冲区偏移地址0×200包含了来自RtlCaptureContext返回的结果:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

从上面可以清楚地看出堆栈指针被泄漏,现在我们需要找到一个能执行控制的地址用以进行重写。注意堆栈顶部:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

这就是当前函数的返回地址。运气不要太好,该偏移地址与其他简单函数将是相同的,所以可以进行写入并使其覆盖其返回地址,从而绕过CFG。

利用补充如下:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

运行时显示EIP控制:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

此外在偏移地址0×40和0×44的写入现在位于栈顶,其允许创建stack pivot于是获得ROP链,接下来就是使用POP EAX gadget,随后使用XCHG EAX,ESP gadget。

解决方案

然而微软表示,通过损坏堆栈上的返回地址以绕过CFG是一个已知的设计限制,因此无法领取任何种类的赏金,如下所示:

如何绕过Windows 10的CFG机制-RadeBit瑞安全

说了这么多,微软做了两个事情来解决这个问题,首先在Windows 10即将到来的版本中,将引进RFG机制(Return Flow Guard),防止通过损坏堆栈以获取执行控制的方式。其次便是在Windows 10的周年版发布中引入敏感的API的介绍,它仅保护微软Edge。虽然对于本例来说没有什么帮助,但是他能限制微软Edge中的RtlCaptureContext API。

Poc代码可以在这找到:https://github.com/MortenSchenk/RtlCaptureContext-CFG-Bypass