基于Chakra JIT的CFG繞過技術
引言
在本文中,我們將向讀者介紹在攻擊Internet Explorer和Edge瀏覽器時可用于繞過Microsoft的控制流防護(CFG)的方法。我們以前的概念驗證性質的漏洞利用代碼是通過覆蓋對象的函數指針來實現的。但是,當遇到CFG時,這種方法就不太好使了。我們假設攻擊者已經獲得了讀寫內存權限。
背景知識
CFG是微軟近來為Windows系統添加一種安全防護機制。該機制通過間接調用/跳轉指令的目標地址的高效檢查來提供保護。如果您希望進一步了解CFG的更多詳情,可以參閱參考文獻[1][2][3],所以我們不做深入細致的講解。
雖然該緩解機制增加了控制流劫持型攻擊的難度,但是CFG本身并不完美。該技術的設計目標是保護間接調用和跳轉,所以,沒有為堆棧(即ROP仍是可能的)提供保護。此外,值得注意的是,這是一個編譯時插樁技術,需要重新編譯源代碼。盡管微軟現在的許多二進制文件可以受益于CFG,但還有很多其他程序不是利用CFG保護機制編譯的。
Chakra JIT
Chakra JIT負責為多次調用的函數和循環生成優化的JIT代碼。這個過程分為多個階段完成,其中Full JIT Compiler和Garbage Collection階段是在后臺線程中進行的。如果您有興趣的話,可以從MSDN上找到相關的工作流程和各種圖釋。
JIT工作流程
我們關注的重點是Full JIT Compiler階段,它負責獲取字節碼和輸出本地代碼。針對單個函數或循環的高級處理是在Func::Codegen()中進行的。首先,它會生成字節碼的中間表示(IR)。然后,這些IR將被轉換若干次:優化、寄存器分配、prolog和epilog等。一旦IR準備就緒,就會被Encoder::Encode()編碼為本地代碼。
- // https://github.com/Microsoft/ChakraCore/blob/master/lib/Backend/Encoder.cpp#L15
- void
- Encoder::Encode()
- {
- NoRecoverMemoryArenaAllocator localAlloc(_u("BE-Encoder"), m_func->m_alloc->GetPageAllocator(), Js::Throw::OutOfMemory);
- m_tempAlloc = &localAlloc;
- ...
- m_encodeBuffer = AnewArray(m_tempAlloc, BYTE, m_encodeBufferSize);
- ...
- }
實際上,真正生成實際本地代碼的任務是由Encoder完成的。首先,它會分配m_encodeBuffer來臨時存放本地代碼。當所有本地指令被發送到m_encodeBuffer之后,Encoder將對該緩沖區進行重新定位,將其復制到read-only-execute內存,并按照CFG的要求處理調用目標。此時,該臨時緩沖區就不再使用,所以可以釋放了。
- // https://github.com/Microsoft/ChakraCore/blob/master/lib/Backend/Encoder.cpp#L294
- ...
- m_encoderMD.ApplyRelocs((size_t) workItem->GetCodeAddress());
- workItem->RecordNativeCode(m_func, m_encodeBuffer);
- m_func->GetScriptContext()->GetThreadContext()->SetValidCallTargetForCFG((PVOID) workItem->GetCodeAddress());
- ...
注意,一旦代碼被復制到可執行內存后,就很難修改了。但是,當Encoder在這個臨時緩沖器中生成本地代碼時,是無法防止攻擊者利用寫入內存權限來更改臨時緩沖器中的代碼的。由于JIT進程位于后臺線程中,所以JavaScript線程仍然可以正常運行。攻擊者的難點是找到該臨時緩沖區,并在Encoder運行的極短時間內完成相應的修改任務。
繞過CFG防護
既然已經知道了修改JIT代碼的基本方法,下面就讓我們付諸行動,以便設法繞過CFG。
我們的過程分為三步:
觸發JIT。
查找臨時的本地代碼緩沖區。
修改緩沖區的內容。
當然,這里隱含的最后一步是執行JIT處理過的代碼。
觸發JIT
第一步,也是最簡單的一步,就是觸發JIT,讓它開始對一個函數進行編碼。為了使第二步變得更容易一些,我們希望函數的代碼多一些,以便我們有足夠的時間在內存中尋找該臨時緩沖區。當然,函數中的具體指令是無關緊要的。
- var code = "var i = 10; var j = 1; ";
- for (var i = 0; i < 6000; i++)
- {
- code += "i *= i + j.toString();";
- }
- code += "return i.toString();"
- f = Function(code);
- for (var i = 0; i < 1000; i++)
- {
- // trigger jit
- f.call();
- }
查找本機代碼緩沖區
一旦后臺線程進入Encoder::Encode(),我們需要快速找到臨時本地代碼緩沖區。發現緩沖區的一種方法是,找到給該緩沖區分配內存的頁分配器,然后逐個查看它分配的內存段。我們注意到,可以先找到ThreadContext,然后找到該后臺線程的BackgroundJobProcessor,這樣就可以找到該頁面分配器的引用了。
- // find the ThreadContext using ThreadContext::globalListLast
- var tctx = readN(jscript9Base + 0x00349034, 4);
- // BackgroundJobProcessor
- var bgjob = readN(tctx + 0x3b0, 4);
- // PageAllocator
- var pgalloc = bgjob + 0x1c;
PageAllocator具有若干已分配段的列表。由于經JIT處理過的函數會變大,所以該臨時本地代碼緩沖器也將很大。所以,通過檢查largeSegments列表,我們就可以輕松找到該內存段了。我們可以使用一個while循環,這樣一直等到這個largeSegments列表變為非空,然后進入最后一步。
- while (true) {
- // read largeSegments list
- var largeseg = readN(pgalloc + 0x24, 4);
- // check if the list was empty
- if (largeseg == pgalloc + 0x24) continue;
- // get the address of the actual data
- var page = readN(largeseg + 8 + 8, 4);
- if (page == 0) continue;
- break;
- }
修改并運行
現在,既然已經知道了臨時本地代碼緩沖區的位置,那么接下來就可以修改其內容來注入shellcode了。當然,按理說只要使用我們的shellcode覆蓋緩沖區的內容就行了,但是實際上要比這個過程要復雜的多,因為我們必須避免覆蓋未來在重定位步驟中將要修改的任何內容。因為用于觸發JIT的函數需要多次調用toString(),同時還要避免重定位的影響,所以,實際上可用于shellcode的空間并不充裕。
雖然最佳之選是修改要進行JIT處理的函數,但這里選擇使用first-stage shellcode,它只是簡單調用VirtualProtect,然后跳轉到我們的second-stage shellcode。這個first-stage shellcode通常是非常小(只有20個字節)的。所以 ,我們可以把first-stage shellcode放到距這個緩沖區比較近的地方,然后在這個緩沖區的起始位置放上一個近轉移指令,從而跳轉至該代碼。
這樣的話,我們的second-stage shellcode可以是任何長度,所以在我們的漏洞利用代碼中,使用了一個metasploit生成shellcode來執行notepad.exe。實際上,這個second-stage shellcode還可以繞過保護模式(沙箱)。
在修改好臨時緩沖區之后,我們將進入最后一步,就是進行等待,直到JIT處理完成并執行修改后的JIT代碼。為此,你可以不斷調用目標函數,直到你的shellcode得到執行為止。
- for (var i = 0; i < 1000; i++)
- {
- // call overwritten jit block
- f.call();
- }
漏洞利用
為了演示這種繞過技術,我們借鑒了此前用于Windows 10上的Internet Explorer 11的利用代碼,并進行了相應的修改。其中,獲取讀寫內存權限的代碼沒有改變,但是在執行我們的shellcode的時候,使用的是JIT代碼覆蓋技術,而不是去覆蓋觸發CFG的函數指針。
你可以在https://github.com/theori-io/jscript9-typedarray-cfg頁面中找到最終的概念驗證漏洞利用代碼。
安全影響
由于該CFG繞過漏洞的影響僅限于攻擊者已經獲得了讀寫內存權限的情況,因此其實用性在現實中可能會受到一些限制。需要引起警覺的是,這個繞過方法具有內置到Chakra JIT架構內部的優勢,這意味著它可能很難修補,并且不會受到像英特爾的CET這樣的未來緩解措施的影響。
補救措施
微軟已經承諾對ChakraCore進行相應的修改,以緩解我們發現的CFG繞過(以及其他CVE補丁)漏洞所造成的威脅。他們的基本思想是,在編碼器編碼指令時計算校驗和,然后在將整個緩沖區復制到最終的RX(read/execute-only)緩沖區之后對校驗和進行驗證。并且,只有通過驗證后,JIT處理后的代碼的入口點才能作為有效的CFG目標。這里的選擇的校驗和算法是CRC32。