深入解析CVE-2018-5002漏洞利用技術
前言
2018年6月1號,360高階威脅應對團隊捕獲到一個在野 ofollow,noindex" target="_blank">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_5和class_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不為0x11111111的cls_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_26和class_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._implGPR和MethodEnv.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