starctf2019-oob
1、环境复现
1、搭建v8调试环境,并下载题目到指定目录(这里需要有v8调试环境搭建的基础,要做好git全局代理和bash全局代理的准备)
2、还原题目环境
1 | cd v8 |
2、diff分析
diff文件如下,其中主要的逻辑在于kArrayOob的具体实现部分,我直接在该段代码中将重要位置进行注释
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
由于创建一个数组长度为length时我们可以访问的是第0到length-1个元素,但是该段代码却直接读取和写入第length个元素,这样就造成了off-by-one。
验证一下,实际上也和我们的分析是一样的
3、分析利用思路
首先编写test.js如下
1 | var a = [1,2,3, 1.1]; |
调试到第一步会给出数组a的object的基址,查看相关job,telescope
1 | gdb d8 |
调试到第二步,打印到了0xf264d68de48位置处的值
1 | pwndbg> c |
调试到第三步,对0xf264d68de48进行了写入
1 | pwndbg> telescope 0x0f264d68de18 |
总结一下,当我们不传参数时,可以泄露object的map字段的值,如果传入参数,传入的参数会写入进object的map字段。
4、编写addressOf和fakeObject原语
what
什么叫做addressOf和fakeObject
计算一个对象的地址addressOf:将需要计算内存地址的对象存放到一个对象数组中的A[0],然后利用上述类型混淆漏洞,将对象数组的Map类型修改为浮点数数组的类型,访问A[0]即可得到浮点数表示的目标对象的内存地址。
将一个内存地址伪造为一个对象fakeObject:将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的Map类型修改为对象数组的类型,那么B[0]此时就代表了以这个内存地址为起始地址的一个JS对象了。
说白了就是一个可以将对象当作地址,一个可以将地址当作对象。我们拿到这个有什么用呢?
why
如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了。
造成上面的原因在于,v8完全依赖Map类型对js对象进行解析。上面这个逻辑希望能仔细理解一下。
通过上面两种类型混淆的方式,就能够实现addressOf和fakeObject。
基于上述分析,如果我们利用oob的读取功能将数组对象A的对象类型Map读取出来,然后利用oob的写入功能将这个类型写入数组对象B,就会导致数组对象B的类型变为了数组对象A的对象类型,这样就造成了类型混淆。
how
下面我们利用JavaScript实现上述addressOf和fakeObject功能原语。
首先定义两个全局的Float数组和对象数组,利用oob函数漏洞泄露两个数组的Map类型:
1 | var obj = {"a": 1}; |
然后实现下面两个函数,下面两个函数就是+1或-1和换map头的过程
addressOf 泄露某个object的地址
1 | // 泄露某个object的地址 |
fakeObject 将某个addr强制转换为object的对象
1 | // 将某个addr强制转换为object对象 |
定义一些gadget,简单的工具代码
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |
5、构造任意读写原语
上面已经将地址转对象和对象转地址的原语构造出来了,现在我们需要利用这两个原语来构造任意地址读写
我们现在有如下的思路:
1、fakeObject可以将给定的内存地址转变为object,我们可以在这块地址上面构造object的结构体,然后利用fakeObject将其转化为object
2、由于这块object完全是我们构造的,所以其中的任何字段都是我们可控的,包括elements字段
3、我们已经知道elements实际上是个指针,指向elements这个object对象的地址,当我们操作数组元素时,其实操作的就是从elements对象地址+0x10的内存,我们有这样一个思路:将elements位置处的值覆盖为任意地址,这样我们操作数组元素时,操作的就是这块写的任意地址的内存。
我们直接通过任意读写原语来具体说明下上述过程
1 | var fake_array = [ // [+0x40] |
描述下上面创建的具体的内存结构
1 | +0x0 elements_map //elements_start |
然后在v8中进行调试
1 | var a = [1.1, 2.2, 3.3]; |
输出结果如下:
1 | pwndbg> r |
6、利用思路归纳
题目的利用思路有两种,一种是通过常规的堆漏洞利用方式,另一种是js引擎漏洞特有的义中叫做wasm的利用方式
在传统堆漏洞的pwn中,利用过程如下(因为在我们的浏览器中,已经实现了任意地址读写的漏洞效果,所以这个传统的利用思路在v8中也同样适用)
通过堆漏洞能够实现一个任意地址写的效果
结合程序功能和UAF漏洞泄露出一个libc地址
通过泄露的libc地址计算出free_hook、malloc_hook、system和one_gadget的内存地址
利用任意地址写将hook函数修改为System或one_gadget的地址,从而实现shell的执行
另外在v8中还有一种被称为webassembly即wasm的技术。通俗来讲,v8可以直接执行其它高级语言生成的机器码,从而加快运行效率。存储wasm的内存页是RWX可读可写可执行的,因此我们还可以通过下面的思路执行我们的shellcode:
利用webassembly构造一块RWX内存页
通过漏洞将shellcode覆写到原本属于webassembly机器码的内存页中
后续再调用webassembly函数接口时,实际上就触发了我们部署好的shellcode
wasm详细见下面的文章,讲的很好:
https://www.jianshu.com/p/bff8aa23fe4d
7、WASM方式进行漏洞利用
简单用法
wasm即webassembly,可以让JavaScript直接执行高级语言生成的机器码。
在线编译网站:https://wasdk.github.io/WasmFiddle/
可以直接通过示例来进行,点击左下角选择Code Buffer,之后正上方build,之后正上方run,之后右下方就会显示出执行结果:
这样有种猜测,是不是可以直接写调用命令,直接转换为WASM码,js引擎直接执行,后来发现这种方式是不行的,报告脚本错误
利用wasm执行shellcode
wasm的作用是将一段功能转换为机器码,实际上wasm是一段AST字节码,之后通过运行wasm这一段字节码将高级语言转换为机器码,也就是说,wasm的功能可以理解为编译功能,wasm的代码和它”编译“生成的机器码的位置是不一样的
所以我们有这样的一种方式:
1、首先通过wasm生成一段tmpcode
2、通过addressOf原语找到存放wasm的内存地址
3、通过任意地址写原语用shellcode替换原本的tmpcode
4、最后调用之前的tmpcode功能即可触发shellcode
寻找
1 | pwndbg> r |
根据上面的思路,写出泄露RWX内存页起始地址的JS代码如下所示:
1 | var shared_info_addr = read64(f_addr + 0x18n) - 0x1n; |
gdb结果如下
1 | pwndbg> r |
这样我们成功的泄露出了rwx内存页的起始地址
后续只要利用任意地址写write64原语我们的shellcode写入这个rwx页,然后调用wasm函数接口即可触发我们的shellcode了,具体实现如下所示:
1 | /* /bin/sh for linux x64 |
最终运行结果如下
1 | root@ubuntu:~/browser_study/v8/v8/v8/out/x64.release# ./d8 exp.js |
完整exp如下
x64如下
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |
x86如下
经过个人分析,x86的exp不可构造
X86构造的过程中,发现在调用oob的过程中,当执行如下语句时
1 | // ××××××××2. addressOf和fakeObject的实现×××××××× |
泄露obj_array_map时发现泄露的是obj_array_map后面四个字节,内存如下
1 | ;obj对象 |
而泄露的内存为00000000260c0695,为obj_array_map的后四个字节,而我们通过调试知道,x86里面变量所占用的存储空间也为8个字节,也就是说,我oob函数读写的时候,所操作的内存空间是8个字节,而起始位置,为0x294083a4,也就正好是越过obj_map,也就是四个字节进行读写。所以我们没有办法操作obj_array_map。
8、传统堆利用方式exp构造
利用步骤
步骤如下:
1、泄露程序本身的地址空间
2、计算d8基址,读取GOT表中malloc等libc函数的内存地址,然后然后计算free_hook或system或one_gadget的地址,最后将system或one_gadget写入free_hook触发hook调用即可实现命令执行
3、调用free(实际上调用了/bin/sh),getshell
泄露地址
可以通过以下顺序:
查看Array对象结构 –> 查看对象的Map属性 –> 查看Map中指定的constructor结构 –> 查看code属性 –>在code内存地址的固定偏移处存储了v8二进制的指令地址
debug日志如下
1 | pwndbg> job 0x27b381f0f8c9 |
编写泄露地址空间地址代码如下
1 | var a = [1.1, 2.2, 3.3]; |
gdb执行如下
1 | pwndbg> r |
计算程序基址以及got表各函数地址
1 | var d8_base_addr = leak_d8_addr -0xfc8780; |
下面逐个进行讲解
获取d8_base_addr
通过下面两张图,可以看到我们泄露的地址为0x0000559f4ba96780,而应用程序的基地址为0x559f4aace000,这样他们的差值就为0xfc8780
获取d8_got_libc_start_main_addr
ida查看.got的偏移,这里是0x12a47b0
获取libc_start_main_addr
获取libc_base_addr
libc的基址可以通过泄露的libc_start_main与偏移进行计算,我们在此次执行中可以知道0x00007f6e88df3fc0是libc_start_main在libc中的地址,0x7f6e88dcd000是libc基址,所以这里的偏移为0x26fc0
获取libc_system_addr和libc_free_hook_addr
ida查看下libc文件,可以看出__libc_start_main确实是在libc基址偏移0x26fc0
所以我们可以直接通过查看ida来确定system和free_hook的偏移,可以看出来分别为0x55410,0x1eeb28
也可以通过gdb中泄露函数或者全局变量的地址,再通过与libc基址相减,即可得到相对应的偏移
最后申请一个变量在进行释放,触发free操作
1 | function get_shell() |
结果如下
也可以发送弹出计算器的命令
1 | function get_shell() |
完整exp如下
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |