再利用Chakra引擎繞過CFG

wyzsk發表於2020-08-19
作者: 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::DeferredParsingThunkNativeCodeGenerator::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 00000000

0:010> dd poi(06eaf050+4) 
06eaf080  00000012 00000000 06e26c00 06e1fea0
06eaf090  5f8db3f0 00000000 5fb0b454 00000101

0: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 00000000

0:010> dd poi(06eaf050+4)
1ce1a0c0  00000012 00000000 06e26c00 06e1fea0
1ce1a0d0  5f8db9e0 00000000 5fb0b454 00000101

0: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;
}

//觸發jit

test1(1);

經過jit的Js::ScriptFunction物件。

#!bash
//新的除錯,物件記憶體地址會不同

0:010> u poi(07103050 )
chakra!Js::ScriptFunction::`vftable':

0:010> dd 07103050 
07103050  5f695580 1d7280c0 00000000 00000000

0:010> dd poi(07103050+4)
1d7280c0  00000012 00000000 07076c00 071080a0
1d7280d0  0a510600 00000000 5fb0b454 00000101

0: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 00000000

0:010> dd poi(06f2cea0+4)
06e6f7a0  00000012 00000000 06e76c00 06f040a0
06e6f7b0  5f8c6130 00000000 5fb0b454 00000101

0: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 00000000

0:010> dd 076690e0 + 10
076690f0  076690e0 04186628 07065f90 00000000

0:010> dd 076690e0 + 28
07669108  07010dc0 000001a8 00000035 00000000

0:010> dd 07010dc0 
07010dc0  5f696000 05a452b8 00000000 5f8db9e0

0: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 參考


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章