作者:jinquan of Qihoo 360 Core Security
作者博客:360 Technology Blog

前言

2018年6月1號,360高級威脅應對團隊捕獲到一個在野flash 0day。上周,國外分析團隊Unit 42公布了關于該次行動的進一步細節。隨后,卡巴斯基在twitter指出此次攻擊背后的APT團伙是FruityArmor APT。

在這篇博客中,我們將披露該漏洞利用的進一步細節。

漏洞利用

原始樣本需要與云端交互觸發,存在諸多不便,所以我們花了一些時間完整逆向了整套利用代碼,以下分析中出現的代碼片段為均為逆向后的代碼。原始利用支持xp/win7/win8/win8.1/win10 x86/x64全平臺。以下分析環境為windows 7 sp1 x86 + Flash 29.0.0.171。64位下的利用過程會在最后一小節簡要提及。

1. 通過棧越界讀寫實現類型混淆

原樣本中首先定義兩個很相似的類class_5class_7,并且class_7的第一個成員變量是一個class_5對象指針,如下:

緊接著調用replace方法嘗試觸發漏洞,可以看到在replace函數內定義了一個class_5對象和一個class_7對象,并將這兩個對象作為參數交替傳入trigger_vul函數()。

從下圖可以看到,trigger_vul方法一共有256個參數,分別為交替出現的128個class_5對象和128個class_7對象。這是為了后面的類型混淆做準備。

trigger_vul內部,首先創建一個class_6對象用于觸發漏洞,

class_6類內調用li(123456)觸發RangeError,通過修改ByteCode后可以導致進入如下的catch邏輯(偽代碼),可以看到在catch內越界交換了兩個棧上的變量(local_448和local_449)。而攻擊者通過精確布控jit棧,導致交換的兩個棧變量恰好為先前壓入的一個cls5對象指針和一個cls7對象指針。從而實現了類型混淆。

成功交換指針后,將修改完后的棧上數據(256個參數)分別回賦給一個cls5_vec對象和一個cls7_vec對象,最后返回cls5_vec對象,這時cls5_vec里面存在一個cls7對象,其余均為為cls5。

在windbg中看到上述過程如下:

根據著色分布可以看到棧上的一個cls5對象指針和一個cls7指針在漏洞觸發后發生了互換:

返回到trigger_vul之后,遍歷cls5_vec中的成員,找出m_p1不為0x11111111cls_5對象,此對象即為被混淆的cls_7。隨后保存有問題的“cls_5”對象和cls_7對象到靜態成員。

trigger_vul返回之后,通過_cls5.m_p6成員是否為0來確定當前環境為x86還是x64,并借助兩個混淆的對象(cls5和cls7)去初始化一個class_8對象,該對象用于實現任意地址讀寫。

2. 任意地址讀寫

class_8類是攻擊者構造的一個工具類,用來實現任意地址讀寫,并在此基礎上實現了x86/x64下的一系列讀寫功能函數。我們重點來看一下readDWORD32和writeDWORD32的實現。

2.1 readDWORD32

由于cls7的第一個成員(var_114)是一個cls5對象,所以在cls5被混淆成cls7后,表面上對cls5.m_p1的修改實質是對cls7.var_114的修改。現在假設我們有一個需要讀取的32位地址addr,只需要把addr-0x10的值賦值給cls5.m_p1,這樣相當于把cls7.var_114設為了addr-0x10。然后去讀取cls7.var_114.m_p1, 此語句會將cls7.var_114.m_p1處的值當做一個class_5對象,并讀取它的第一個成員變量,也即將addr-0x10當作一個class_5對象,并讀取addr-0x10+0x10處的四個字節。

下圖解釋了為什么32位下需要addr-0x10,由于繼承關系,每一個as3對象的前16個字節結構是固定的(其中,“pvtbl”是C++虛表指針,“composite”、“ vtable”和“delegate”成員可以參考avmplus源碼中的ScriptObject實現),一個類對象的第一個成員變量位于對象首地址+0x10處(64位下類推為addr-0x20):

圖:從內存來看,混淆后,對cls5的操作實際上影響了cls7對應內存處的值,隨后可以通過訪問cls7.var_114.m_p1去讀取任意addr處的值。

2.2 writeDWORD32

writeDWORD32原理和readDWORD32類似,此處不再贅述。

在clsss_8類中,攻擊者在上述兩個函數的基礎上實現了一系列功能函數,全部如下:

3. 定位ByteArray相關成員偏移

雖然攻擊者并未借助ByteArray來實現任意地址讀寫,但為方便利用編寫,他必須知道當前Flash版本中ByteArray相關成員的內存偏移。為此,攻擊者定義了一個class_15類,用來借助任意地址寫實現對特定成員的偏移搜索并,保存。以供后面使用。

setOffset32的部分邏輯:

以下class_15的成員用來保存動態搜索到的內存偏移。

4. 1st shellcode

找到相關偏移后,攻擊者立即開始構造shellcode并執行。 1階段的shellcode為內置,但有7個DWORD32字段需要動態填充。而2階段的shellcode通過一個ByteArray動態傳入,即上面setOffset函數中的_bArr成員。由于并未得到攻擊者的2階段shellcode,我們使用的2階段shellcode來自HackingTeam泄漏的代碼,功能為彈一個計算器。

攻擊者先借助ByteArray(ba)存儲了一個1階段shellcode模板,反匯編后如下,其中紫色區域是需要動態填充的字段,這些字段代表的含義如注釋所示:

然后初始化一個新的ByteArray對象(ba2),將其的array區域的前16字節初始化如下:

5. Bypass ROP

為了構造ROP,攻擊者專門定義了一個輔助類class_25,在里面實現了如下功能函數:

攻擊者先借助flash模塊的IAT找到User32.dll的GetDC地址,再借助User32.dll的IAT找到ntdll.dll的RtlUnWind地址,

隨后從ntdll.dll的EAT的AddressOfFunctions數組中找到NtProtectVirtualMemory和NtPrivilegedServiceAuditAlarm的函數偏移并計算得到對應的函數地址。

攻擊者這里的思路是取出NtProtectVirtualMemory的SSDT索引,和NtPrivilegedServiceAuditAlarm+0x5的地址,供后面使用。

后面會通過call NtPrivilegedServiceAuditAlarm+0x5并傳入NtProtectVirtualMemory的SSDT索引的方式來Bypass ROP的檢測。由于ROP檢測并未Hook NtPrivilegedServiceAuditAlarm作為關鍵函數,所以并不會進入ROP檢測邏輯中,因此繞過了ROP的所有檢測。

隨后搜索以下的ROP部件并保存,供后面使用

隨后將上述信息返回給上層調用者:

隨后部分值被填充到1st shellcode的前5個pattern。

6. Bypass CFG

這個樣本在32位下通過覆蓋jit棧的方式來繞過CFG,攻擊者首先定義了兩個相似的類class_26class_27。兩者都定義了一個方法叫做method_87。不同之處在于class_26.method_87只接受兩個參數,而class_27.method_87接受256個參數,并會將傳入的參數全部保存并返回給調用者。

6.1 jit地址替換

攻擊者首先初始化了一個class_26對象cls26和一個class_27對象cls27。然后借助任意地址讀寫能力將cls26.method_87的jit地址替換為cls26.method_87的jit地址,

然后第二次調用cls26.method_87,此時實際上調用的是cls27.method_87,由于cls26.method_87自身只會傳入2個參數,導致泄漏了大量jit棧上的數據,攻擊者隨后利用泄漏的數據找到一個jit參數棧的地址,并第二次調用cls27.method_87,用以覆蓋jit棧的一個返回地址,從而在對應的函數返回時控制eip。

在windbg中觀察一下上述過程:

6.2 jit地址替換原理

根據這篇文章,我們可以知道cls26對象的+0x08處是一個vTable對象指針,而vTable對象的+0x48處是一個MethodEnv對象指針,MethodEnv對象內又包含自身的_implGPR函數指針和一個MethodInfo對象指針,MethodInfo對象內也包含一份_implGPR函數指針,這些結構體間在內存中的尋址關系如下所示:

所以replace_jit_addr函數本質上是用cls27.method_87的jit地址替換了cls26.method_87的jit地址。但cls26.method_87的jit地址在好幾個地方都有存儲(如上圖就有MethodEnv._implGPRMethodEnv.MethodInfo._implGPR兩個地方存儲著cls26.method_87的地址),我們如何確定要覆蓋的是哪一個地方?

這得從class_21$/executeShellcodeWithCfg32函數的jit匯編代碼中尋找答案。如下是executeShellcodeWithCfg32的部分匯編代碼。代碼中紅框圈出的兩句代碼清楚地指明了cls26.method_27函數第二次調用時的函數指針尋址過程,很明顯,這里用的是MethodEnv._implGPR

至于cls27.method_27的地址,任意找一個存儲其jit地址的地方讀取即可(這里也可以采用HackingTeam的代碼中讀取jit函數指針的方法,如下)。所以一共可以有三種方式。Exp代碼中的兩種,加上HackingTeam中的一種。但寫入地址是唯一的。通過上述做法,成功實現了對jit地址的偷天換日。

在2016年的一篇總結Flash利用的文獻中,作者曾介紹過用覆寫MethodInfo._implGPR的方式來劫持eip。兩種方式十分類似,但并不完全相同。

6.3 覆寫jit棧上的返回地址

在第二次調用cls27.method_87時,攻擊者傳入的參數如下,其中的retn為上面尋找到的gadget03(addr_of_ret)。其余重要參數均在注釋中進行說明。由于ba2_array的前12個字節分別為:第一階段的shellcode地址(ba_array),0x1000,0。這些恰好對應NtProtectVirtualMemory所需的前3個參數。

我們具體看一下cls27.method_87內部的邏輯。可以看到若第一參數為0x85868788,則遞歸調用自身20次,這是為了布局jit棧,方便后面覆蓋eip:

在最后一次調用中,cls27.method_87會借助前面泄漏的jit棧地址來找到將要覆蓋的eip所在的棧地址pRetAddr,并保存原始返回地址。

隨后,為了在觸發漏洞后不造成crash,攻擊者又傳入原始返回地址第二次修改1st shellcode,將最后兩個pattern處填寫為正確的值,保證shellcode執行完后可以正常返回:

通過覆蓋棧上的eip劫持控制流,成功避開了CFG的檢測,從而Bypass CFG。

調試發現被覆蓋的eip為jit棧上cls27.method_87遞歸調用自身20次中某次的返回地址

最后,在遞歸調用某次返回的過程中,eip被成功劫持至第一階段的ROP,隨后的整個過程在windbg中觀察如下:

2nd shellcode執行完畢后,會繼續從class_27.method的遞歸調用中返回。然后返回到flash的正常邏輯,此過程中不會造成crash和卡頓,整個利用方式非常穩定。

7. 64位下的利用分析

原利用代碼也支持64位環境。64位下的漏洞觸發代碼和32位下并沒有什么不同,只在Bypass CFG部分有所差異。原利用代碼中出現了兩種Bypass CFG的方法,下面分別介紹。

7.1 分支1

如果當前64位環境下的ntdll.dll中可以找到如下gadget,則走分支1。從注釋的匯編代碼中可以清楚地看到這部分gadget的作用:彈出棧頂部的4個值給x64調用約定下作為前4個參數的寄存器并返回。

隨后找到kerner32!VirtualProtect函數地址,并和傳入的shellcode一起傳入下圖所示的函數,在curruptJitStack函數借助jit地址覆蓋去替換返回地址(此過程和32位下非常相似),并在jit函數返回時利用rop將shellcode所在地址設置為可執行。隨后調用replaceJitApply64去調用執行shellcode。replaceJitApply64函數內借助了HackingTeam之前泄漏的方法去Bypass CFG,即覆蓋FunctionObject.Apply()方法的虛表地址。其中replaceJitApply64方法會在分支2中分析。

7.2 分支2

假如在當前進程的ntdll.dll沒有找到分支1所需的gadget,則進入分支2,分支2采用了覆蓋FunctionObject.Apply()方法的虛表地址的方法。

我們來詳細看一下replaceJitApply64,如果熟悉之前HackingTeam的利用代碼,則很容易理解下述代碼:

分支2會兩次調用replaceJitApply64函數,第一次的目的是調用kernel32!VirtualProtect函數去設置shellcode的執行權限。函數內首先定義一個ByteArray對象ba,然后將shellcode放置在ba.array的首部。

隨后將找到ExecMgr對象的虛表,將其虛表前的8個字節及虛表的前0xE4/8個虛函數地址拷貝到ba.array的len(shellcode)起始處(偽造虛表)。

隨后覆蓋偽造的ExecMgr虛表+0x30處的8個字節,這正是apply方法對應的虛函數地址。隨后覆寫ExecMgr首部的虛表指針,設置相關寄存器的值和相關對象偏移處的值,以構造VirtualProtect函數所需的4個參數,隨后調用apply方法以調用VirtualProtect,調用完將之前覆蓋的值都恢復原來的值,從而不造成crash。對這部分細節的詳細描述可以參考這篇博客。下圖的注釋也寫得比較清楚。

調用完后返回到上級函數,隨后再次調用replaceJitApply64方法,用shellcode+0x8的地址去替換apply方法對應的虛函數地址。從而執行shellcode。執行完shellcode后回到Flash代碼,整個過程也不會造成crash。

總結

CVE-2018-5002是一個位于avm2解釋器內的非常嚴重的漏洞,漏洞質量高,影響范圍極為廣泛。從原始flash的編譯日志可以觀察到,整套利用框架早在2018.2.7日就已經完成編譯。該套利用代碼通用性強,穩定性好,整體水平較高。

References

https://recon.cx/2012/schedule/attachments/43_Inside_AVM_REcon2012.pdf


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/702/