Chrome浏览器漏洞入门
前言
开坑Chrome浏览器漏洞,找个简单的开始入手。
跟PHP的UAF玩法很像,触类旁通不难理解。
WebAssembly
简单来说就是为了提高性能,而将C等语言编译成一段类似字节码一样的指令,并在浏览器上执行。
根据官方文档和参考来简单认识一下这门技术,首先去GitHub下载Emscripten,用于将简单的程序编译成WebAssembly。
开始安装:
1 |
|
安装完成:
然后编译一个简单的测试html和配套js:
1 |
|
编译结果:
可以看到会编译完成三个文件,一个html一个js和一个wasm,可以看出WebAssembly的运行机制就是由js调用wasm实现使用C等语言实现的代码。一般来说为了安全考虑,wasm不会允许用户直接调用系统函数(比如printf),wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。这种通过emcc编译的方式出来的结果会比较复杂,但是可以将printf这种函数调用编译成wasm可以执行的内容,具体不明。
用在线编译弄个不调用函数的简单测试:
1 |
|
编译生成字节码后然后再用js调用:
1 |
|
执行js就可以看到控制台上打印了81。
v8环境搭建
通过starCTF2019的一道题来入门,首先要搭建v8(chrome浏览器js引擎)环境。
听说因为墙的关系安装起来很麻烦,所以根据参考文章,先去vultr弄个国外主机(64G硬盘、2G内存,centos7,centos8要装python2似乎有点麻烦了),然后复制黏贴命令:
1 |
|
切换其他漏洞分支可以用于学习其他漏洞,参考。
编译时间很长,还遇到了swap区不够大的问题,参考文章。
编译完成后将整个v8目录打包,然后ssh也好,http也好,ftp也好,找个办法把包下载到虚拟机解压,包有点大差不多3G,整个文件夹解压出来大概9G。(我是通过xshell进行下载的,宿舍奇怪的网络早上下载比晚上快多了)
然后还要配置一下pwndbg,先把v8 gdb的初始化文件放到用户目录下(好像也没必要?不过算了):
1 |
|
然后像pwndbg的配置一样,让gdb启动时同时启动v8 gdb,修改用户目录下gdb的初始化文件.gdbinit,加入:
1 |
|
然后写个js用于测试:
1 |
|
然后:
1 |
|
运行脚本可以看到:
1 |
|
v8调试环境搭建成功,更新/重新编译方式:
1 |
|
或许可以用来切换分支之后重新编译?晚点再试试。
v8基础知识
基本类型在内存中的存储
首先是几个基础类型,包括整型(Smi/immediate small integer)、浮点数(Double)及指针(Pointers),在内存中保存的形态,按照参考文章中的描述:
1 |
|
为了区分数字和指针指向的地址,v8将地址的最低位置为1,所以指针指向的地址-1才是该对象在内存中的真正地址。
然后来看看v8 gdb的几个基础调试知识。
allow-natives-syntax选项
在gdb中:
1 |
|
来使用。
主要定义了两个调试用函数:
- %DebugPrint(a),用于打印对象地址
- %SystemBreak(),相当于断点
job命令
将JavaScript对象在内存中的数据打印成可视形态,比如打印某个对象:
1 |
|
经过测试我们可以看出,当一个js对象中同时存在整型和浮点型,他们的存储方式就是浮点式。而如果存在对象类型,存储的浮点数和对象会用指针进行表示。
telescope
将某处地址处的数据打印出来:
1 |
|
可以看出,一个对象的内存中的前三个部分分别为:map属性,properties属性(对象成员保存在这里)和elements属性(数组成员保存在这里)。
查看一下数组的elements属性:
1 |
|
可以看到,elements属性的三个部分分别是:map属性、成员数量和成员。
类型辨认
我们可以注意到,我们使用job命令读取一个地址时可以将其可视化打印成相应的数字或者对象,其中必定经过了类型辨认的阶段。
整型很好辨认,其低32位全是0,浮点数和对象的辨认就需要依靠他们的map属性了。
如果对象中的成员全是数字(整型/浮点数),那就不需要每个成员都带一个map属性,再用指针来表示了。
当成员中存在对象时,指针会指向该成员的map属性。(浮点数变成map+浮点的存储形式,对象不变)
对象及其elements属性的map属性
仔细观察可以发现,对象和elements属性各有一个map属性,里面都有类型相关的标识,那么在访问elements属性时,会以哪个属性为准呢?
尝试在gdb中将一个对象数组的map属性修改为一个浮点型数组的map属性(整型只有32位,无法完全读取),修改一下测试脚本:
1 |
|
然后修改内存(用不了job命令会稍有麻烦,不过问题不大):
1 |
|
输出不是我们想看到的地址形式,需要做下类型转换:
1 |
|
最后得到:000018a9c7bcddf9,即对象数组中成员所在的elements属性(elements指针指向的0x18a9c7bcde80地址处),中的第一个成员0x18a9c7bcddf9(位于地址0x18a9c7bcde90处)。
顺带一提,如果调试的是debug版本,修改内存之后再打印对象会发现程序错误,大概是这个样子:
参考文章2也有提到这个问题,这里我就不加以研究了。
starCTF2019
题目地址:https://github.com/0xfocu5/CTF/blob/master/Chrome/2019-starctf-oob.zip
题目提供了一个diff文件对源代码进行了修改,首先我们要通过这个diff文件对我们下载到的源代码进行更新和重编译。
环境搭建
用vultr上的国外云主机更新一遍,首先把diff文件放到v8目录下,执行:
1 |
|
然后查看src/bootstrapper.cc:
可以看到补丁已经打上去了,然后我们重新编译出d8:
1 |
|
然后把out目录打包到本地即可,简单测试一下oob函数能不能调用:
1 |
|
没有问题,环境搭建完成。
oob函数
看看这个函数的具体实现:
1 |
|
简单来说就是根据参数数量执行读或写操作,而C++的成员函数的第一个参数是this指针,所以实际上就是无参执行读操作,有1个参数时执行写操作。仔细观测对数组进行的读写操作,访问的都是array[length],而正常数组的下标都是0至length-1,所以这里实际上存在一个越界访问问题,会对。
我们通过调试一下看看:
1 |
|
附近的内存:
1 |
|
很明显,这里读写的其实就是对象数组的首个地址单元的数据,即其map属性的地址。
也就是说,通过这个越界写,我们可以让v8的类型判断混乱,从而让一个对象数组被认为是一个浮点数组:
1 |
|
结果就会打印出第一个成员对象的地址:
1 |
|
整合一下成一个函数:
1 |
|
调试确认没有问题:
1 |
|
任意地址读写
通过这个类型混淆,我们能做什么呢?
将对象数组混淆成浮点数组,我们可以打印一个对象的地址。
而反过来,将浮点数组混淆成对象数组,然后访问其第一个成员,v8就会将这个浮点数当成一个对象地址。而因为这个浮点数可以由我们随意控制,所以如果我们可以控制某一块内存的数据,我们就可以伪造一个对象出来。
这个问题也很好解决,我们可以设置一个浮点数组,因为浮点数组的成员是成片排列在内存中的,所以这样一来我们就可以完全控制一块内存用以伪造对象。然后通过addressOf函数获取其内存地址,通过调试我们可以发现elements一般紧贴该对象,所以可以通过计算获取这片内存的地址,先调试一下看看偏移:
1 |
|
结果:
1 |
|
因为elements属性前两个地址单元放的是map属性和成员数量,第三个地址单元开始才是成员,所以再计算上0x10的偏移,最后结果应该是-0x38+0x10。
通过伪造一个浮点数组,我们可以读取其成员,即elements属性所指向地址后方一点地方的数据,再简单测试一下,把伪造浮点数组的长度加多一个0(长度变为16),同样由于elements指针和具体成员之间存在一个0x10的偏移,修改一下伪造浮点数组的第三个浮点数:
1 |
|
输出结果:
1 |
|
可以看到,我们成功获得了当时被设置在浮点数组map属性位置,对象数组的map属性地址。
同样写成函数:
1 |
|
顺便把任意地址读写也写成函数:
1 |
|
我们已经实现了任意地址读,任意地址写也很简单,直接修改fake_obj即可。
命令执行
有了任意地址读写,我们可以做什么呢?
与PHP的UAF不同,chrome没有什么可以执行系统命令的函数,无法直接通过调用函数的方式执行命令。
不过chrome中存在wasm技术,虽然wasm编译后的结果看起来是一段v8字节码而不是系统汇编代码,不过当它加载到v8中的时候,v8会不会生成一些会执行到的汇编代码在内存中?如果有,这段地址就是可读可写可执行的,我们就可以通过任意地址写将其替换成我们的shellcode从而完成命令执行。
我们先来调试看看wasm加载字节码之后形成的函数是什么样子的:
1 |
|
结果:
1 |
|
先看看很可疑的code属性:
着实可疑,再看看权限:
不可写,算了算了。context属性看起来是内置相关的东西,然后看看shared_info:
1 |
|
看起来跟wasm有关,它的code跟上一级对象一致,还有一个from data的标注,那看看它的data,即WasmExportedFunctionData(看起来就是跟导入的字节码有关):
1 |
|
wrapper_code同样跟上一级对象的code属性一致,看看instance:
1 |
|
打印内存看看:
1 |
|
我们可以发现,在偏移0x88处存在一个地址,而该地址段可读可写可执行,接下来我们需要确认这一段代码会被执行到,调用一下从字节码中加载的函数f,然后继续执行:
1 |
|
可以看到,往这里写代码后确实是可以执行的。
我们需要用任意地址读获取这个地址:
1 |
|
然后用任意地址写把我们的命令执行shellcode写入即可,不过直接用write函数写入的时候有点问题,因为写入时需要将指针指向-0x10偏移的地方,而这个偏移对于这段汇编代码来说是一个不可读的区域,所以就会导致程序崩溃。
解决的方法来自参考文章:
完整利用
1 |
|
运行结果:
总结
总的看一遍下来,还是挺好玩的,不过没有读过v8源码,有点地方还是不甚清晰。
相比PHP的UAF玩法,chrome的越界读写漏洞操作起来更简单一些,理解难度也要低一些。