StarCTF 2019 (*CTF) oob 初探V8漏洞利用
开始学习浏览器安全。首先从这道*CTF 2019的oob题目来入门Chrome V8引擎的基础利用。
本文参考资料:从一道CTF题零基础学V8漏洞利用 这篇文章写的非常详细
另外,在这之前需要对V8中的JS对象实现有基础的了解
我把一些基础和参考资料记录在了:Chrome V8 学习笔记(一)
调试准备
1 | git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598 |
然后编辑out.gn/x64.release/args.gn
,加入以下内容,以支持job等命令打印对象。
1 | v8_enable_backtrace = true |
最后ninja编译
1 | ninja -C out.gn/x64.release d8 |
如果不修改args.gn内容的话,这样直接编译出来的d8在gdb中是不能用job打印对象内容的。
或者也可以用这个命令编译
1 | tools/dev/gm.py x64.release |
分析diff文件
diff中的关键部分,增加的oob()方法
1 | diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc |
看一下源码,猜一下一些函数的作用。。
类型参考
1 | // /src/objects/fixed-array-inl.h |
大概可以知道,新增的JSArray.oob()
函数允许对对象的elements
数组进行一个单位大小的越界读写。
数组内存布局
定义两个数组,在gdb中查看内存布局
1 | var obj = {loveyou:"sunxiaokong"}; |
内存布局如下,以floatArray为例,可以看到floatArray的elements数组最后一个元素elements[0]紧接着就是floatArray的map。因此,oob()方法实际上可以做到对数组对象map的读写。
另外,这里是使用[]方括号来声明数组的,得到的内存布局就如下,即对象的elements在对象的前面。但是我一开始用的是new Array()的方法来声明数组,得到的内存布局中,elements会放在对象的后面。
1 | //run in gdb |
类型混淆
通过修改map指针,可以做到类型混淆。例如,修改floatArray的map指针为objArray的map指针,则v8会将floatArray中的float元素当成object指针去处理,反之,则可将objArray中的object指针当成float给泄漏出来。
定义两个函数addressOf()
和fakeObject()
实现这两个功能:
1 | //leak an object pointer |
再定义一些类型转化的函数,方便在浮点数和无符号整数之间转换
1 | /* type convert */ |
任意地址读写
可以通过将数组对象的elements数组伪造成一个数组对象,实现任意地址读写。
示意图如下。在elements[0]写入一个数组的array,elements[2]写入目的地址-0x10,然后再用fakeObject()方法将elements[0]的地址伪造成一个对象fake_array,再对fake_array[0]进行读写,实际上就是对目标地址进行读写了
1 | elements ____________________ |
代码实现:
1 | var floatArrayAddr = addressOf(floatArray); |
执行测试代码后,输出结果如下,成功做到任意地址读写
传统CTF的利用
不稳定泄漏
传统CTF中很多时候可以通过往free_hook
或写入system
地址或onegadget
达到getshell的目的,这道题同样。
首先要泄漏出地址,根据 https://www.freebuf.com/vuls/203721.html 中提示,根据泄漏出来的obj地址,可以向上很远的地方(objaddr-0x8000)开始查找一个d8地址空间中的地址,该处地址对应的指令为push rbp
,可以看到这样的地址是不止一个的的。
因此,可以先选定一个地址,取低三位(即使开启地址随机化,低12位仍是恒定的偏移),只要从objaddr-0x8000开始循环读一个地址出来判断低12位,若符合我们选定地址的第三位,再读出地址里的内容,若目标地址内容为0x56415741e5894855,则可确定该地址即为d8装载空间中的地址,根据相对偏移即可确定d8装载基地址,再从got表中泄漏libc地址,即可完成内存泄漏
1 | /*-------------------------------- leak --------------------------------*/ |
然后往free_hook中写入system的地址,这里有一个问题,用之前写好的write64是无法完成写入的,原因可能与float数组对浮点数的处理有关。。这里 https://www.freebuf.com/vuls/203721.html 作者写了一个用dataView实现的write64操作,可以完成写入,方法是修改dataView对象中的buffer对象的backing store指针(buffer对象中的backing store即为分配到的堆块的实际地址)为目的地址,从而实现任意地址写。
1 | var dataBuf = new ArrayBuffer(8); |
用这个write64Dataview(freeHook, systemAddr);
方法可以成功将system地址写入到free_hook
然后定义一个函数中的局部变量,内容为字符串/bin/sh
,函数退出后,这个局部变量会被垃圾回收机制释放,即可触发system('/bin/sh')
,最后还可以将free_hook改回去,这样退出shell后可以比较正常的退出,不然由于内存中一直存在的垃圾回收还会执行很多system(),可能会在目录下生成一些垃圾文件。
1 | function pwnYou() |
EXP
整理后的EXP如下
1 | // myoobexp1.js |
缺点
这种利用方式,地址泄漏不稳定,而且我不往out.gn/x64.release/args.gn
中写入支持调试的参数,重新编译d8之后,d8地址空间中的got表地址、偏移等会发生变化,因此这种利用方式根本做不到稳定通用的利用。
稳定泄漏
https://www.freebuf.com/vuls/203721.html作者介绍了另一种稳定泄漏的方式
运行以下实例代码
1 | var testArray = [1.11]; |
查看constructor对象
查看constructer中的code对象
可以看到,有一条指令中包含了一个d8地址空间的地址,只要将code+0x40处的指令读出来,再右移两个字节16位即可得到一个d8中的地址。用这个方法的内存泄漏代码:
1 | /*------------------------------leak d8------------------------------*/ |
成功泄漏d8基址,后面的操作就和刚才一样了,通过改freehook为system地址可getshell
EXP
整理EXP
1 | //myoobexp2.js |
成功弹出计算器
尝试pwn chrome
刚才的EXP是针对单独的v8引擎的,后来我针对题目附带的chrome改了一下got表等偏移量,将js嵌入html,成功往chrome进程的free_hook写入了system的地址,但是仍然无法弹出计算器,不知道为什么
可以看到确实是往freehook写入了system的地址,然后一会就崩掉了。。
不知道是什么原因不能正常弹计算器,如有师傅知道原因,恳请指正!
利用WASM执行shellcode
可以通过WASM,能得到一块RWX的内存,里面放着WASM的二进制代码,将shellcode写入到这块内存,再调用WASM接口时,就会执行Shellcode了。
首先要找到这块RWX内存,运行以下测试代码
1 | var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1, |
通过以下嵌套查找可以找到这块地址:
f->shared_info->data->instance+0x88
。调试器中具体看一下:
1 | job 0x2534e961fb01 <----- funciton f |
因此,可以通过addressOf和read64、write64等方法实现以下代码完成地址泄漏,并写入Shellcode
1 | var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1, |
成功本地getshell
EXP
1 | /*---------------------leak object and fake object---------------------*/ |
chrome
把shellcode换成/bin/ls
:
可以用pwntools写一个:
1 | #shellcode.py |
1 | //sys_execve('/bin/ls', argv, 0) argv = {"ls", 0} |
把这个shellcode换进EXP里,再把exp.js放入一个html的<script> </script>
标签中,用题目给的Chrome带上--no-sandbox
选项以无沙箱模式打开就可以啦。
EXP.html
1 | <script> |
控制chrome打开GUI应用的问题
刚才用的是/bin/ls
来进行漏洞利用的验证,其实我也试过比如execve('/usr/bin/gedit')
打开图形化应用,但是会报错
好像是和execve()
的第三个参数envp[]
有关,暂时还没有解决这个问题。。
总结
总结以上漏洞利用过程:
1 | 数组越界 -> 读写map指针 -> 类型混淆 -> 任意地址读写 -> WASM执行Shellcode/劫持free_hook |
- 存在数组越界漏洞
- 通过能够读写一个对象的map指针
- 通过读写map指针,能够做到对象类型的混淆、伪造。
- 通过将一个地址伪造成对象,实现了任意地址读写。
- 通过WASM写入并执行Shellcode或劫持free_hook完成最终利用
可以看到,比较关键的步骤在于类型混淆这里,这就要求对v8中JS对象的实现、内存布局等有一定的了解。
感谢阅读 ♪(^∇^*)
- 本文链接:http://www.sunxiaokong.xyz/2020-01-13/lzx-starctf-oob/
- 版权声明:本站文章除特别声明外,均为本站原创或翻译,采用 知识共享署名 CC BY 4.0 国际协议进行许可,转载前请务必署名及注明出处。