0x00 概述
作者:Francisco Falcón
地址:https://blog.coresecurity.com/2015/03/25/exploiting-cve-2015-0311-part-ii-bypassing-control-flow-guard-on-windows-8-1-update-3/
三月初我们发布了一篇分析CVE-2015-0311(Flash Player的UAF)的博文,我们概述了如何在Windows 7 SP1上进行利用。我们在博文的末尾提到,该博文叙述的利用过程并不适用于较新版本的Windows,如Windows 8.1 Update 3,原因是新操作系统平台所采用的的漏洞利用缓解技术-–|控制流防护(CFG)。
CFG-–|微软于2014年11月在Windows 8.1 Update 3上推出的一种技术-–|在每一个间接调用前都增加了一个检查,用以检查所调用的目的地址是否是编译时所确定的“安全”位置之一。如果运行时检查失败,程序就认为存在试图改变正常执行流程的操作,并立即退出。
在CFG出现之前,利用诸如UAF之类的漏洞进行任意内存读写等基本操作–如我们在CVE-2015-0311中所做的–通常意味着需要绕过ASLR和DEP然后最终稳定执行代码。CFG的特别之处就在于,即使能够进行内存的读写等操作,最终想要执行代码还是需要付出可观的额外努力。
事实证明集成到Windows 8.1 Update 3中的IE11的Flash版本在编译时启用了CFG,因此通用的利用方法–将对象虚函数表(vtable)覆盖为指向攻击者控制的数据的指针,然后调用对象虚函数的方法不再可用。
下面来看一下我们是如何绕过CFG对Win8.1 Update 3上IE11的Adobe Flash Player进行漏洞利用的。这篇文章假定你已经读过part I(Exploiting CVE-2015-0311: A Use-After-Free in Adobe Flash Player),因此跳过CVE-2015-0311本身的细节的讨论,直接进入到劫持浏览器进程的执行流上来。
0x01 CFG技术简介
下图所示,就是在没有启用CFG的Flash版本中,调用Vector对象的toString()方法时间接引用被覆盖的虚函数表(vtable)的代码,上一篇博文中也提到过:
而下面是Win8.1 Update3的Flash 16.0.0.287(编译时启用CFG)执行同样操作的代码:
___|guard_check_icall_fptr函数指针指向**ntdll!LdrpValidateUserCallTarget**,该函数负责检查间接调用的目的地址是否合法。当检查到假的虚函数表,改变正常执行流的企图就会被发现,然后程序转向**ntdll!RtlpHandleInvalidUserCallTarget**函数,该函数通过发出一个INT 29h的中断立刻结束进程。
如想了解更多关于CFG内部机制,我推荐看一下MJ去年在Power Of Community安全会议上的演讲“Windows 10 Control Flow Guard Internals”
0x02 解决方法
CFG技术是通过在编译时添加大量对防护函数的调用以保护每一个间接调用的。Win8.1 Update 3的Flash Player 16.0.0.287包含了29238次对防护函数的调用。:
那么,就存在如下几种可以在CFG启用的情况下修改执行流的方法:
- 覆盖返回地址。
- 利用进程中未使用CFG的模块。
- 找到因为某种原因而未被CFG保护的间接调用。
第一种方法要求能够获取到一个栈地址。尽管在改变了Vector长度后获得了任意读写能力,在只知道指向堆中对象的指针的情况下,对于栈地址我还是没有办法获取到。
第二种方法会在利用中引入受感染软件之外的新的依赖,因此这种方法可能也不是很理想。
我找到了一些未被CFG保护的间接调用,如下例所示:
该函数指针位于.data段中,而数据段是具有读写权限的,因此覆盖该地址绝对是有可能实现的。然而,这种方法的主要问题在于你必须能够影响程序的执行流程(在还没有得到代码的执行能力之前),以使修改后的函数指针最终是可被调用的。那么如果该函数仅在一些非常见的情况下调用(罕见的极端情况下调用)怎么办?又或者如果该函数仅在进程初始化的早期调用怎么办?
使用这种方法的另一个障碍在于如果被覆盖的函数指针被调用时没有CPU寄存器指向我们自己的数据附近,想要转换栈以开始运行ROP链将会变得非常棘手。
目前为止,CFG在保护超过29000个Adobe Flash Player的间接调用上做的已经很好了。那么现在的问题就变为:我们能否在Flash Player中找到未被CFG保护的间接调用,并且能够以简单的方式影响程序的执行流程,使其调用被覆盖的函数指针?
如前面所述,CFG只保护那些可在编译时确定的间接调用。下一个问题自然就变成:Flash Player中有不在编译时生成的间接调用吗?答案是肯定的:当然有!
Flash Player包含有一个JIT(Just-In-Time)编译器,可以即时将ActionScript虚拟机字节码翻译成本机的机器码以提高执行速度。JIT生成的代码中存在间接调用,又因为代码是在运行时生成的,因此这些代码里的间接调用并未被CFG保护。
0x03 寻找未被保护的间接调用
让我们回到上一篇文章漏洞利用过程中的某处,在该处我们可以修改Vector对象的元数据,我们可以通过调用si32(0xffffffff,0x24)来设置Vector的长度为0xffffffff,而这个新的长度可以让我们在进程的地址空间中读写任意内存数据。同时记得我们构造了一个ByteArray对象,我们的ROP链就保存为Vector的第一个元素。而存储在ByteArray+8位置的dword就是指向虚函数表对象的指针(类的真实名称就是VTable_object,在core/VTable.h中定义):
现在,让我们仔细检查一下VTable_object,我们可以看到它包含了很多指针:
我们试着跟进几个指针(在下面的图片中我跟进了VTable_object + 0xD4位置的指针),就会发现它们都看起来都差不多一样。这些都是MethodEnv对象(在core/Method.h中定义的):
MethodEnv对象中的第一个dword是指向其虚函数表的指针,第二个dword是一个函数指针(本例中为0x601C0A70)
MethodEnv对象VTable_object + 0xD4位置的指针被间接引用,从而产生一个间接调用,调用MethodEnv_object+4(0x601c0a70)处的函数指针,我们注意到,此处的间接调用代码是Flash JIT编译器生成的,因此是未被CFG保护的!而且这个未保护的间接调用是可以通过调用ByteArray对象的toString()方法稳定触发的。
我们可以在VTable_object + 0xD4位置设一个硬件断点,当我们的ActionScript代码调用this.the_vector[0].toString()时,由JIT生成的代码读取VTable_object + 0xD4位置的指针,该断点就会命中。注意此处函数指针是从ECX+4中取出(MOV EAX, DWORD PTR DS:[ECX+4]),并直接地进行调用,并未先调用CFG防护函数进行安全验证:
位于堆中的由JIT生成的代码,是通过如下路径执行到的:首先BaseExecMgr::invokeGeneric方法(core/exec.pp中定义)调用BaseExecMgr::endCoerce:
#!c++
// Invoker for native or jit code used before we have jit-compiled,
// or after JIT compilation of the invoker has failed.
Atom BaseExecMgr::invokeGeneric(MethodEnv *env, int32_t argc, Atom* atomv)
{
MethodSignaturep ms = env->get_ms();
const size_t extra_sz = startCoerce(env, argc, ms);
MMgc::GC::AllocaAutoPtr _ap;
uint32_t *ap = (uint32_t *)avmStackAlloc(env->core(), _ap, extra_sz);
unboxCoerceArgs(env, argc, atomv, ap, ms);
return endCoerce(env, argc, ap, ms);
}
然后BaseExecMgr::endCoerce调用JIT生成的函数(该函数我们在上上段中讲述过,其中包含未受CFG保护的间接调用):
#!c++
Atom BaseExecMgr::endCoerce(MethodEnv* env, int32_t argc, uint32_t *ap, MethodSignaturep ms)
{
[...]
switch(bt){
[...]
default:
{
STACKADJUST(); // align stack for 32-bit Windows and MSVC compiler
const Atom i = (*env->method->_implGPR)(env, argc, ap);
[...]
如下是BaseExecMgr::endCoerce的二进制代码:
0x04 利用
我们已经了解了如何触发一个未受CFG保护的间接调用,现在我们需要做的是在VTable_object + 0xD4位置放一个虚假的MethodEnv对象。有图有真相,我们可以在下图中直观地看到对象之间的初始状态以及调用关系(点击图片可放大):
在ActionScript利用代码中,我们通过Nicolas Joly的基于Number对象的读取方法可以获取到ByteArray object + 8位置的指针,如上一篇博文中提到的:
#!c++
var vtable_object:uint = leak_8_bytes(bytearray_object_pointer + 8);
然后指针值 + 0xD4得到我们想覆盖的目标地址,之后再计算出Vector对象(其长度我们已经覆盖为0xffffffff)中我们需要使用的定位,以写入目标地址:
#!c++
var target_address:uint = vtable_object + 0xd4;
/* 0x28: offset of the first element within the Vector object */
var idx: uint = (target_address - (address_of_vector + 0x28)) / 4;
this.the_vector[idx] = address_of_rop_chain >> 3;
我们将VTable_object + 0xD4位置存放的指针覆盖为ROP链(存储在上面的ActionScript代码片中的 address_of_rop_chain
变量中)的地址。同时注意我将address_of_rop_chain
右移了三次;这是因为 address_of_rop_chain
是unit类型的,而ActionScript虚拟机内部存储机制中,unit类型的值是要先左移3次然后再异或6(6代表“Integer”标签-–|我们上一篇博文中提到过)。
最后,我们只需调用下ByteArray对象(存储于this.the_vector[0])的toString()方法,就可以触发JIT生成代码中的未经CFG保护的间接调用,然后就会启动我们的ROP链。
#!c++
new Number(this.the_vector[0].toString());
注意,未保护的间接调用CALL EAX 调用我们控制的函数指针时,ECX寄存器就指向我们的ROP链,因此pivoting stack(一种栈转换技术–指向的堆空间变成新的栈)开始执行ROP链就变得很简单。
0x05结论
控制流防护(CFG)是一种有用的缓解技术,它可以增加利用代码编写的难度和成本。但是就像任何一种已知的漏洞利用缓解技术一样,在特定的环境下,其不可避免的存在绕过的方法。
本文介绍的绕过方法,利用了JIT生成代码中未经保护的间接调用,该方法不仅适用于Adobe Flash Player,任何使用了JIT编译器的软件都潜在地可以如此利用,因为运行时生成的代码是不会受到CFG保护的,除非开发者能够付出额外的努力将JIT编译器生成的代码固化。
请注意Flash Player的JIT编译器其实之前就已经被利用过不止一次,用以绕过各种缓解机制:
Dion Blazakis 2010年的“Pointer inference and JIT spraying”
Fermin Serna 2013年的“Flash JIT – Spraying info leak gadgets”