最新的漏洞利用開始漸漸脫離基于ROP的代碼重用攻擊。在過去的兩年里,出現了一些關于一種新的代碼重用攻擊的文章,Counterfeit Object-Oriented Programming(COOP)。COOP是一種頂級的針對forward-edge的執行流完整性(CFI) 的攻擊方式。在我們把CFI解決方案(HA-FI)整合進我們的終端產品中時,這種攻擊吸引了我們的注意力。COOP主要出現在學術界,還沒有出現在exloit工具包里。這也許是因為攻擊者更趨向于使用更簡單的方法。在win10年度更新中微軟的Edge使用了執行流保護(CFG,Control Flow Guard)。在CFG中,缺少backward-edge的CFI更容易受到攻擊。但是當Return Flow Guard (RFG)出現,使得攻擊者不能再依靠淹沒棧中的返回地址進行攻擊的時候,會發生些什么呢?
我們對評估COOP在攻擊CFI時的效果很感興趣。這不僅可以使我們保持在學術界和黑客社區中前沿研究中的地位,也可以測試產品的有效性,更改設計,甚至在必要時普遍地提高我們自己的防御能力。在我們的這一系列的兩篇博文中的第一篇中,介紹了我們使用COOP函數重用對微軟的CFG以及我們自己的HA-CFI進行攻擊的評測。
微軟執行流保護
已經有大量的論文,博文和會議發言充分地討論了微軟的執行流保護(CFG,Microsoft’s Control Flow Guard)。Trail of Bits在兩篇最近的帖子里比較了Clang CFI和微軟CFG。第一篇帖子著重Clang,第二篇強調微軟對CFI的實現,還有額外的研究提供了CFG的實現的進一步細節。
在過去的幾年里,繞過CFG也成為了安全會議中中一個流行的主題。在我們引用一些著名的繞過方法之前,最重要的是CFI能夠進一步分為兩種:forward-edge和backward-edge。
Forward-Edge CFI: 保護間接調用或是JMP位置. Forward-edge CFI解決方案包括微軟 CFG和Endgame的HA-CFI.
Backward-Edge CFI: 保護返回指令. Backward-edge CFI解決方案包括微軟的Return Flow Guard,Endgame的DBI exploit防護的一部分, 以及包括intel的CET在內的其他ROP檢測。
這個分類幫助我們描繪出了CFG保護位置的輪廓——間接調用位置——以及不打算保護的位置——棧返回地址。例如,一個最近的POC入選了exploit工具包,這個POC針對Edge,使用讀/寫的原始方法來修改棧中的返回地址。但這并不適用于CFG,不應該作為CFG的弱點來考慮。盡管如此,它成功地證明了CFG的有效性,并使攻擊者轉向劫持執行流,而不是間接調用的位置。這個例子實際上證明了CFG缺陷包括以下幾點:利用未受保護的函數調用位置,重映射包括CFG代碼在內的只讀內存區域,并使他們指向需要受到檢查的代碼,在Charka中提到的JIT編碼器的資源競爭,使用基于內存的間接調用。COOP或是函數重用攻擊,在面對CFI的實現時有著公認的局限,因為“limitations of coarse-grained CFI”,他們并沒有入選微軟的bypass賞金。也就是說,我們不知道有哪些公有領域的POCs能證明COOP能指定攻擊CFG的加固的二進制代碼。
CFG對每個受保護的DLL添加了一個__guard_fids_table,它由一系列在二進制代碼中合法的RVAs或是間接調用指令中敏感的目的地址組成。一個地址作為CFG bitmap索引的一部分而存在。bitmap里的bits能夠根據地址是否是合法的目的地址而進行切換。在此之外,也有一個API能夠對bitmap進行修改,例如,為了支持JIT編碼的頁面:
kernelbase!SetProcessValidCallTargets在使用系統調用更新bitmap之前會調用ntdll!SetInformationVirtualMemory
win10創意者更新有一項新增的功能可以抑制導出,也就是說,現在導出函數能在CFG保護的調用位置被標記為非法目的地址。這一功能的實現需要使用CFG Bitmap中每一個地址的第二位,以及在初始化每個進程的bitmap時__guard_fids_table中每一個RVA條目的一個標記字節。
對于64位的系統,地址的第9-63位被用于在CFGbimap中檢索一個qword,第3-10位被用于(模64)訪問qword中某一指定位。在導出被抑制后,CFG允許一個給定的地址在CFG bitmap中用兩位表示。此外,在大多數DLLs中__guard_dispatch_icall_fptr現在被設置為指向ntdll!LdrpDispatchUserCallTargetES,在其中一個合法的調用目標必須從CFG bitmap中刪去。
當你把動態解析符號表考慮進去的時候,實現這樣一個導出表抑制變得有點復雜,因為使用GetProcAddress意味著隨后的代碼也能調用返回值作為函數指針。只要CFG bitmap中每一個條目沒有被標記為敏感的或是不合法的(例如,VirtualProtect, SetProcessValidCallTargets等等),執行流保護可以通過把條目對應的兩位從“10”(導出表抑制)改為“01”(合法的調用位置),解決這個問題。最后,一些導出表將會在進行創建時以不合法的間接調用開始,但最終在運行時代碼中成為合法的調用目的地址。在今后我們的討論中,這尤為重要。當這一情況發生時,一個調用棧的樣例如下:
00 nt!NtSetInformationVirtualMemory
01 nt!setjmpex
02 ntdll!NtSetInformationVirtualMemory
03 ntdll!RtlpGuardGrantSuppressedCallAccess
04 ntdll!RtlGuardGrantSuppressedCallAccess
05 ntdll!LdrGetProcedureAddressForCaller
06 KERNELBASE!GetProcAddress
07 USER32!InitializeImmEntryTable
COOP概要
Schuster et al.認為COOP是CFI實現的一個潛在的弱點。為了在繞過forward-edge CFI的檢查之后執行代碼,我們可以利用連續的攻擊序列和重用已存在的虛函數。在ROP在有一個相似的方法,其結果是一系列小段合法函數,每一段代碼實現最低限度的功能(例如,載入一個值進RDX中),但把它們組合在一起,卻可以實現一些復雜的任務。COOP的一個基本組成部分就是利用主循環函數,在其中可以迭代對象鏈表或數組,調用每個對象中的虛函數。然后,攻擊者把內存中“偽裝”的對象組合起來,在某些情況下,可能會覆蓋對象,這樣就能在主循環中按攻擊者安排好的順序調用合法的虛函數。Schuster et al.證明了使用COOP payloads的攻擊win7 32位和64位上的IE10,以及Linux 64位上的Firefox的方法。這項研究隨后被擴展了,證明了遞歸或是帶有許多非直接調用的函數也可以實現這一過程,而不僅僅是循環。隨后又繼續被擴展到用于攻擊Objective-C 運行時環境。
這項前沿研究極其有趣和新奇。我們想要把這一概念應用到一些現代的CFI實現上,以對如下方案進行評估:a)在加固的瀏覽器中構造一個COOP payload的難度;b)是否能繞過CFG和HA-CFI;c)是否能改進CFI使其能檢測到COOP類型的攻擊。
我們的目標
我們使用COOP主要的目標是win10的Edge,因為它代表著一個全新的CFG加固應用,并且它能讓我們在內存中使用JavaScript來準備我們的COOP payload。弱點始終是我們小組的興趣,為了這個目標,我們專注于劫持CFI的執行流,并對攻擊者作出了下列假設:
1.任意的讀-寫原語都是從JavaScript中獲得的。
2.因為在運行時動態地找到小段代碼不是這項研究的內容,因此,允許使用硬編碼偏移量。
3.所有微軟創意者更新中最近的防御機制都能被使用(例如,ACG,CIG,帶導出表抑制的CFG)。
4.除了使用COOP以外,攻擊者不允許以任何方式繞過CFG。
在我們最初的研究里,我們在對微軟年度更新(OS build 14393.953)中的Edge的研究中利用了一個Theori中的POC,我們使用創意者更新中的防御機制設計我們的payload,并在開啟導出表抑制的win10創意者更新(OS build 15063.138)中對其進行驗證。
一個理想的POC會執行一些攻擊者的shellcode或是啟動一個應用程序。攻擊者的一個經典的代碼執行模型,就是把一些內存中被控制的數據映射為+X,然后跳轉到包含最新修改過的+X區域的shellcode。然后,我們的真實目的是在forward-edge CFI的保護下,產生一個能夠執行一些有意義的代碼的COOP payloads。這樣一個payload提供了能夠進行測試和改善我們的CFI算法的數據。進一步說,攻擊Arbitrary Code Guard (ACG)或是Edge的子進程的辦法超出了我們的研究范圍。我們確定對于win10創意者更新研究的最終目標是使用COOP來使CFG無效,使得在DLL內能夠跳轉或是調用任意位置的代碼。因此,我們總結出下面兩個主要的COOP payloads:
1.對于win10年度更新,以及缺少ACG保護的程序,我們的payload把我們的控制的數據映射為可執行的代碼,在使得CFG無效后跳轉到我們控制的shellcode所在區域。
2.對于win10創意者更新,我們的最終目標是僅僅是使CFG無效。
尋找COOP片段
下列Schuster et al.設想的藍圖,我們的第一業務是商定COOP各個組成部分的術語。學術論文將每個重用函數稱為虛函數片段(virtual function gadget)或是vfgadget,當我們描述每一個特定類型的vfgadget時使用縮寫,例如將主循環(main loop)vfgadget稱為ML-G。我們選擇以更為非正式的方式來命令每種類型的gadget。在接下來的帖子中你能找到的術語定義如下:
Looper:對于執行復雜COOPpayloads(論文中的ML-G)至關重要的主循環gadget。
Invoker:一個調用vfgadget的函數指針。(論文中的INV-G)
Arg Populator:帶一個參數的虛擬函數,它將一個值加載到寄存器中(論文中的LOAD-R64-G),或是移動棧指針或是把值加載進棧中(論文中的MOVE-SP-G)
與論文相似,我們編寫了腳本來幫助我們識別二進制中的vfgadgets。我們使用了IPA Python,推理幫助我們找到了loopers,invokers和argument pupulators。在我們的研究中,我們發現了實現COOP的實用的方法就是,在返回到JavaScript之前,把vfgadgets鏈接到一起并依次執行少量的vfgadgets。根據需要通過額外的COOP payloads重復這個過程。因此,為了我們的目的,我們發現沒有必要將二進制代碼提升到IR。然而,將大量COOP payload拼接到一起,比如說完全通過重用代碼運行一個C2 socket線程,也許會需要提升到IR。對于vfgadget的每個子類型,我們定義了一系列規則,并使用它在Edge(chakra.dll和edgehtml.dll)的兩個二進制文件間進行搜索。這些規則中與looper vfgadget相關的一部分包括:
1.出現在__guard_fids_table中的函數
2.包含一個不帶參數的間接調用的循環
3.循環不能影響到參數寄存器
在vfgadgets的所有類中,搜索loopers是最耗時的。許多潛在的loopers有一些限制使其難以使用。我們尋找到的invokers不僅需要有調用虛函數指針的vfgadgets,還要能夠在單一的counterfeit對象中,一次性又快又容易地填充六個參數的vfgadgets。因此,當嘗試調用單個API時,COOP可以使用快捷方式,完全避免對循環和遞歸的需求,除非需要返回值。在x64程序上能夠找到許多寄存器對參數寄存器進行填充。值得一提的是,Schuster et al.的COOP論文中根據mshtml提出的大量原始vfgadgets仍然能在edgehtml中找到。然而,我們在我們的成果中添加了一個要求來避免重用這些,而不是為我們的COOP payloads尋找新的vfgadgets。
COOP Payloads
通過腳本語言觸發COOP,我們實際上能把一些復雜的任務從COOP中移開,因為一次性把所有東西拼接在一起非常的復雜。我們能使用JavaScript來幫助我們,重復調用微型COOP payload序列。這也讓我們能把諸如算術和條件操作放回JavaScript中執行,并保留基本的函數重用來為通過COOP調用重要的API做準備。此外,我們展示了這種方法的一個例子,包括在我們劫持到的#1 section中將COOP的返回值傳回到JavaScript,并討論如何調用LoadLibrary。
為了簡潔,我將只介紹最簡單的payloads。payloads的一個公共的主題是需要調用VirtualProtect。因為VirtualProtect和eshims(譯者注:應該是ieshims)APIs被標記為敏感的且在CFG中并不是一個合法的目的地址,我們不得不在創意者更新中使用包裝函數。正如Thomas Garnier所建議的那樣,可以在.net庫mscoree.dll和mscories.dll中方便地找到包裝函數,例如UtilExecutionEngine::ClrVirtualProtect。因為微軟的ACG可以防止創建新的可執行內存,以及把已有可執行內存改為可寫,因此,我們需要一個替代方法。使用VitualProtect可以把只讀內存重映射為可寫的,所以我借用了2015年黑帽大會里介紹的這種技術,并將包含chakra! __guard_dispatch_icall_fptr的頁面重新映射為可寫,然后重寫函數指針,使其指向包含jmp rax指令的chakra.dll中的任意位置。事實上,在大多數DLL中已經存在一個函數__guard_dispatch_icall_nop,它剛好就是一個單一的jmp rax指令。因此,我就能有效地繞過CFG的保護,因為在通過了所有檢查之后,在chakra.dll中所有被保護的調用位置將立即跳轉到目的地址。想必我們可以采用這種方法進一步探索使用函數重用攻擊ACG的方法。為了完成這個小小的鏈接過程,需要以下滿足以下條件:
1.把mscoroc.dll載入進Edge進程
2.在chakra.dll的只讀內存區域調用ClrVirtualProtect +W
3.重寫__guard_dispatch_icall_fptr以通過檢查
從上面的vfgadgets列表可以看出,對于COOP來說edgehtml是一個重要的庫。因此,我們的第一任務就是泄漏edgehtml的基址以及其他必要的組件,例如我們的counterfeit內存區域。這樣,payload就能包含硬編碼的偏移并在運行時重新定位。使用Theori的POC中泄漏的bug,我們就能獲得我們想要的基地址。
//OS Build 10.0.14393
var chakraBase=Read64(vtable).sub(0x274C40);
var guard_disp_icall_nop=chakraBase.add(0x273510);
var chakraCFG=chakraBase.add(0x5E2B78); //_guard_dispatch_icall...
var ntdllBase=Read64(chakraCFG).sub(0x95260);
//Find global CDocument object, VTable, and calculate EdgeHtmlBase
var [hi, lo]=PutDataAndGetAddr(document);
CDocPtr=Read64(newLong(lo + 0x30, hi, true));
EdgeHtmlBase=Read64(CDocPtr).sub(0xE80740);
//Rebase our COOP payload
rebaseOffsets(EdgeHtmlBase, chakraBase, ntdllBase, pRebasedCOOP);
觸發COOP
使用COOP的一個關鍵部分就是在最初把JavaScript傳遞進looper中。使用我們假設的R/W原語,我們可以輕易地劫持到chakra的vtable,使其指向我們的looper,但我們怎么確保looper會開始迭代我們counterfeit的數據呢?對于這個答案,我們需要進looper進行評估,在這里我使用了CTravelLog::UpdateScreenshotStream:
注意在循環前的第一個塊中,代碼是在+0x30處獲取到鏈表的指針。為了正確啟動looper,我們需要劫持JavaScript對象的vtable,使其地址包含在我們的looper 中,然后在對象+0x30處放置一個指針使其指向counterfeit對象列表的首部。實際的counterfeit對象數據可以通過JavaScript進行定義和重新定位。還要注意,循環在對象+0x80h處的的下一個指針列表處進行迭代。當構造counterfeit流時這很重要。此外,請注意,這個間接調用的位置在vtable+0xF8h處。在counterfeit對象中的任意偽vtable都必須指向設計好的函數指針減0xF8h處,這個地址通常是在鄰接vtable表的中間。為了啟動COOP的payload,我劫持了JavascriptNativeIntArray對象,并地freeze()和seal()虛函數進行了重載,如下所示:
本文由 看雪翻譯小組 夢野間 編譯,來源 Matt Spisak@Endgame
作者:叫我詹躲躲
轉發鏈接:https://juejin.im/post/5edb6c6be51d4578a2555a9b