xlab · 2015/12/24 15:00
Author:[email?protected]
0x00 前言
本文源自一次與TK閑聊,期間得知成功繞過CFG的經過與細節(參考:[利用Chakra JIT繞過DEP和CFG])。隨即出于對技術的興趣,也抽出一些時間看了相關的東西,結果發現了另一處繞過CFG的位置。所以這篇文章中提到的思路與技術歸根結底是來自TK提示的,在此特別感謝。
關于CFG的分析文章已經有很多了,想要了解的話可以參考我之前在HitCon 2015上的演講(spartan 0day & exploit)。要說明的是,本文的內容即為我演講中馬賽克的部分,至此通過一次內存寫實現edge的任意代碼執行方法就全部公開了。
0x01 Chakra調用函數的邏輯
chakra引擎在函數調用時,會根據所調用函數狀態的不同進行不同的處理。比如第一次調用的函數、多次調用的函數、DOM接口函數及經過jit編譯后的函數。不同的函數類型會有不同的處理流程,而這些不同的處理都會通過Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >
函數調用Js::JavascriptFunction::CallFunction<1>
函數來實現。
1.函數的首次調用與多次調用
當調用如下腳本時,Js::JavascriptFunction::CallFunction<1>
函數會被Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >
函數調用。
#!js
function test(){}test();
復制代碼
如果函數是第一次被調用,則執行流程如下。
#!bash
chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >|-chakra!Js::JavascriptFunction::CallFunction<1>|-chakra!Js::JavascriptFunction::DeferredParsingThunk|-chakra!Js::JavascriptFunction::DeferredParse|-chakra!NativeCodeGenerator::CheckCodeGenThunk|-chakra!Js::InterpreterStackFrame::DelayDynamicInterpreterThunk|-jmp_code|-chakra!Js::InterpreterStackFrame::InterpreterThunk
復制代碼
如果再次調用這個函數的話,調用流程如下。
#!bash
chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >|-chakra!Js::JavascriptFunction::CallFunction<1>|-chakra!NativeCodeGenerator::CheckCodeGenThunk|-chakra!Js::InterpreterStackFrame::DelayDynamicInterpreterThunk|-jmp_code|-chakra!Js::InterpreterStackFrame::InterpreterThunk
復制代碼
兩次的調用流程大致是相同的,其中主要不同是因為,函數在第一次調用時候需要通過DeferredParsingThunk函數對其進行解析。其實函數只有在第一次調用時才進行進一步的初始化解析操作,這樣設計主要是為了效率。而后續調用再直接解釋執行。
分析發現,Js::JavascriptFunction::CallFunction<1>
函數調用的子函數是通過Js::ScriptFunction對象中的數據獲得的。后續調用的函數Js::JavascriptFunction::DeferredParsingThunk
和NativeCodeGenerator::CheckCodeGenThunk
都存在于Js::ScriptFunction
對象中。兩次調用中Js::ScriptFunction
對象的變化。
第一次調用時的Js::ScriptFunction對象。
#!bash
0:010> u poi(06eaf050 )
chakra!Js::ScriptFunction::`vftable':0:010> dd 06eaf050
06eaf050 5f695580 06eaf080 00000000 000000000:010> dd poi(06eaf050+4)
06eaf080 00000012 00000000 06e26c00 06e1fea0
06eaf090 5f8db3f0 00000000 5fb0b454 000001010:010> u poi(poi(06eaf050+4)+0x10)
chakra!Js::JavascriptFunction::DeferredParsingThunk:
復制代碼
第二次調用時的Js::ScriptFunction對象。
#!bash
0:010> u poi(06eaf050 )
chakra!Js::ScriptFunction::`vftable':0:010> dd 06eaf050
06eaf050 5f695580 1ce1a0c0 00000000 000000000:010> dd poi(06eaf050+4)
1ce1a0c0 00000012 00000000 06e26c00 06e1fea0
1ce1a0d0 5f8db9e0 00000000 5fb0b454 000001010:010> u poi(poi(06eaf050+4)+0x10)
chakra!NativeCodeGenerator::CheckCodeGenThunk:
復制代碼
所以函數在第一次調用與后續調用的不同,是通過修改Js::ScriptFunction對象中的函數指針來實現的。
2.函數的jit
接下來我們看一下函數的jit。測試腳本代碼如下,多次調用test1函數觸發其jit。
#!js
function test1(num)
{return num + 1 + 2 + 3;
}//觸發jittest1(1);
復制代碼
經過jit的Js::ScriptFunction
對象。
#!bash
//新的調試,對象內存地址會不同0:010> u poi(07103050 )
chakra!Js::ScriptFunction::`vftable':0:010> dd 07103050
07103050 5f695580 1d7280c0 00000000 000000000:010> dd poi(07103050+4)
1d7280c0 00000012 00000000 07076c00 071080a0
1d7280d0 0a510600 00000000 5fb0b454 000001010:010> u poi(poi(07103050+4)+0x10) //jit code
0a510600 55 push ebp
0a510601 8bec mov ebp,esp
0a510603 81fc5cc9d005 cmp esp,5D0C95Ch
0a510609 7f21 jg 0a51062c
0a51060b 6a00 push 0
0a51060d 6a00 push 0
0a51060f 68d0121b04 push 41B12D0h
0a510614 685c090000 push 95Ch
0a510619 e802955b55 call chakra!ThreadContext::ProbeCurrentStack2 (5fac9b20)
0a51061e 0f1f4000 nop dword ptr [eax]
0a510622 0f1f4000 nop dword ptr [eax]
0a510626 0f1f4000 nop dword ptr [eax]
0a51062a 6690 xchg ax,ax
0a51062c 6a00 push 0
0a51062e 8d6424ec lea esp,[esp-14h]
0a510632 56 push esi
0a510633 53 push ebx
0a510634 b8488e0607 mov eax,7068E48h
0a510639 8038ff cmp byte ptr [eax],0FFh
0a51063c 7402 je 0a510640
0a51063e fe00 inc byte ptr [eax]
0a510640 8b450c mov eax,dword ptr [ebp+0Ch]
0a510643 25ffffff08 and eax,8FFFFFFh
0a510648 0fbaf01b btr eax,1Bh
0a51064c 83d802 sbb eax,2
0a51064f 7c2f jl 0a510680
0a510651 8b5d14 mov ebx,dword ptr [ebp+14h] //ebx = num
0a510654 8bc3 mov eax,ebx //eax = num (num << 1 & 1)
0a510656 d1f8 sar eax,1 //eax = num >> 1
0a510658 732f jae 0a510689
0a51065a 8bf0 mov esi,eax
0a51065c 8bc6 mov eax,esi
0a51065e 40 inc eax //num + 1
0a51065f 7040 jo 0a5106a1
0a510661 8bc8 mov ecx,eax
0a510663 83c102 add ecx,2 //num + 2
0a510666 7045 jo 0a5106ad
0a510668 8bc1 mov eax,ecx
0a51066a 83c003 add eax,3 //num + 3
0a51066d 704a jo 0a5106b9
0a51066f 8bc8 mov ecx,eax
0a510671 d1e1 shl ecx,1 //ecx = num << 1
0a510673 7050 jo 0a5106c5
0a510675 41 inc ecx //ecx = num += 1
0a510676 8bd9 mov ebx,ecx
0a510678 8bc3 mov eax,ebx
0a51067a 5b pop ebx
0a51067b 5e pop esi
0a51067c 8be5 mov esp,ebp
0a51067e 5d pop ebp
0a51067f c3 ret
復制代碼
Js::ScriptFunction
對象中原本指向NativeCodeGenerator::CheckCodeGenThunk
函數的指針,在jit之后變為指向jit code的指針。實現了直接調用函數jit code。
這里簡單說明一下,在調用函數傳遞參數時,是先將參數左移一位,然后將最低位置1之后的值進行傳遞的(parameter = (num << 1) & 1)。所以其在獲取參數之后的第一件事是將其右移1位,獲取參數原始的值。至于為什么要這樣,我想應該是因為腳本引擎垃圾回收機制導致的,引擎通過最低位來區分對象與數據。
#!bash
chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >|-chakra!Js::JavascriptFunction::CallFunction<1>|-jit code
復制代碼
調用jit函數時的調用棧如上所示,這就是chakra引擎調用jit函數的方法。
3.DOM接口函數
最后為了完整性,還有一類函數需要簡單介紹一下,就是DOM接口函數,由其它引擎如渲染引擎提供的函數(理論上可以為任何其它引擎)。
#!js
document.createElement("button");
復制代碼
執行上面腳本則會通過下面的函數調用流程,最后調用到提供接口函數的引擎中。
#!bash
chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >|-chakra!Js::JavascriptFunction::CallFunction<1>|-chakra!Js::JavascriptExternalFunction::ExternalFunctionThunk //調用dom接口函數|-dom_interface_function //EDGEHTML!CFastDOM::CDocument::Trampoline_createElement
復制代碼
當調用dom接口函數時,Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >
函數及后續的處理流程中所使用的Function對象與前面不同,使用的是Js::JavascriptExternalFunction
對象。然后與前面的函數調用類似,也是通過解析對象內的函數指針,并對其進行調用,最終進入到想要調用的DOM接口函數中。
#!bash
0:010> u poi(06f2cea0)
chakra!Js::JavascriptExternalFunction::`vftable':0:010> dd 06f2cea0
06f2cea0 5f696c4c 06e6f7a0 00000000 000000000:010> dd poi(06f2cea0+4)
06e6f7a0 00000012 00000000 06e76c00 06f040a0
06e6f7b0 5f8c6130 00000000 5fb0b454 000001010:010> u poi(poi(06f2cea0+4)+0x10)
chakra!Js::JavascriptExternalFunction::ExternalFunctionThunk:
復制代碼
這就是chakra引擎對不同類型函數的不同調用的方式。
0x02 漏洞與利用
經過前面對chakra引擎各種調用函數方法的介紹,我們再來看一下本文的重點繞過cfg的漏洞。前面提到的在第一次調用腳本創建的函數時與后續調用此函數會有不同的流程。這里我們再看一下此處的邏輯,調用棧如下。
#!bash
//第一次調用
chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >|-chakra!Js::JavascriptFunction::CallFunction<1>|-chakra!Js::JavascriptFunction::DeferredParsingThunk|-chakra!Js::JavascriptFunction::DeferredParse //獲取NativeCodeGenerator::CheckCodeGenThunk函數|-chakra!NativeCodeGenerator::CheckCodeGenThunk|-chakra!Js::InterpreterStackFrame::DelayDynamicInterpreterThunk|-jmp_code |-chakra!Js::InterpreterStackFrame::InterpreterThunk
復制代碼
在前面沒有提到的是,上面調用流程中的Js::JavascriptFunction::DeferredParse
函數。此函數內部會進行函數解析相關的工作,并且返回NativeCodeGenerator::CheckCodeGenThunk
函數的指針,然后在返回Js::JavascriptFunction::DeferredParsingThunk
函數后對其進行調用。NativeCodeGenerator::CheckCodeGenThunk
函數的指針也是通過解析Js::JavascriptFunction
對象獲得的。代碼如下。
#!js
int __cdecl Js::JavascriptFunction::DeferredParsingThunk(struct Js::ScriptFunction *p_script_function)
{NativeCodeGenerator_CheckCodeGenThunk = Js::JavascriptFunction::DeferredParse(&p_script_function);return NativeCodeGenerator_CheckCodeGenThunk();
}.text:002AB3F0 push ebp
.text:002AB3F1 mov ebp, esp
.text:002AB3F3 lea eax, [esp+p_script_function]
.text:002AB3F7 push eax ; struct Js::ScriptFunction **
.text:002AB3F8 call Js::JavascriptFunction::DeferredParse
.text:002AB3FD pop ebp
.text:002AB3FE jmp eax
復制代碼
在這個跳轉位置上并沒有對eax中的函數指針進行CFG檢查。所以可以利用其進行eip劫持。不過還首先還要知道Js::JavascriptFunction::DeferredParse
函數返回的NativeCodeGenerator::CheckCodeGenThunk
函數指針是如何通過Js::ScriptFunction
對象何解析出來的。解析過程如下。
#!bash
0:010> u poi(070af050)
chakra!Js::ScriptFunction::`vftable':0:010> dd 070af050 + 14
070af064 076690e0 5fb11ef4 00000000 000000000:010> dd 076690e0 + 10
076690f0 076690e0 04186628 07065f90 000000000:010> dd 076690e0 + 28
07669108 07010dc0 000001a8 00000035 000000000:010> dd 07010dc0
07010dc0 5f696000 05a452b8 00000000 5f8db9e00:010> u 5f8db9e0
chakra!NativeCodeGenerator::CheckCodeGenThunk:
復制代碼
如上所述,Js::JavascriptFunction::DeferredParse
通過解析Js::ScriptFunction
對象獲取NativeCodeGenerator::CheckCodeGenThunk
函數指針,解析方法簡寫為[[[Js::ScriptFunction+14]+10]+28]+0c
。所以只要偽造此處內存中的數據,即可通過調用函數來間接觸發Js::JavascriptFunction::DeferredParse
函數的調用,進而劫持eip,具體如下。
#!bash
0:010> g
Breakpoint 0 hit
eax=603ba064 ebx=063fba10 ecx=063fba40 edx=063fba40 esi=00000001 edi=058fc6b0
eip=603ba064 esp=058fc414 ebp=058fc454 iopl=0 nv up ei ng nz na po cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000283
chakra!`dynamic initializer for 'DOMFastPathInfo::getterTable''+0x734:
603ba064 94 xchg eax,esp
603ba065 c3 ret
復制代碼
這樣就繞過了cfg,成功劫持了eip。這種方法簡單穩定,在獲得了內存讀寫能力時使用是很方便的。此漏洞已于2015年7月25日報告微軟。
0x03 修補方案
本文所述的漏洞微軟已經修補,修補方案也比較簡單就是對此處跳轉增加cfg檢查。
#!bash
.text:002AB460 push ebp
.text:002AB461 mov ebp, esp
.text:002AB463 lea eax, [esp+arg_0]
.text:002AB467 push eax
.text:002AB468 call Js::JavascriptFunction::DeferredParse
.text:002AB46D mov ecx, eax ; this
.text:002AB46F call ds:___guard_check_icall_fptr //增加cfg檢查
.text:002AB475 mov eax, ecx
.text:002AB477 pop ebp
.text:002AB478 jmp eax
.text:00
復制代碼
0x04 參考
- 利用Chakra JIT繞過DEP和CFG
- spartan 0day & exploit