前言:
v8里面的一些trick
,整理一下。
正文:
建议写exp在release版本上写,debug版本上会出现许多check错误,导致会出现许多莫名其妙的错误,比如修改了一个
map
值就会报错。当JIT的时候,创建一个空数组的时候,如果在JIT传入参数是一个定值,那么就会产生
LoadElements
结点,此时产生的checkbounds
边界根据输入来参考。如果传入参数索引处为undefined
,那么产生的结点便是LoadProperty
,此时不会产生checkbounds
。想要漏洞利用的时候,尽量最好用一个去改另一个再用另一个继续去接下来的利用。比如一个在JIT中的OOB,如果直接用函数里的array,并且只用这一个漏洞array改写自己的length去接下来的利用,那么情况会很复杂..比如会有空间不稳定性,后面所声明的array或者buff与这个漏洞array之间的空间距离是很不稳定的,乱跳的。而且gc的情况也会很复杂。所以最好用一个去写另一个,再用另一个去构造exp。
当不是两个连续var的时候,两个array的相对距离一般是不稳定的,也就是不会是固定偏移,最好是连续var才会有固定偏移。
千万不要用固定偏移去写exp,即使用搜索内存的方法去用也不要去算固定偏移,因为很可能gdb中是固定的(固定也不是100%概率),但到了真实环境中,很可能根本就不会是这个值,所以就会产生gdb中利用好了,本机确没办法用的情况。。
Debug:
一般用到两个:
1 | %DebugPrint() //用来打印v8对象 |
普通调试的时候可以直接gdb
到d8
上,再结合c
和ctrl c
配合使用调试。
还有一些:
DebugBreak():
1 | 当分析v8源码时,遇到CodeStubAssembler编写的代码,可以在其中插入DebugBreak();这相当于插入了一个断点(类似int 3),重新编译后使用调试器调试时,可以在插入处断下。 |
Print():
1 | 遇到CodeStubAssembler编写的代码时,可以使用它来输出一些变量值,函数原型是: |
readline():
1 | 可以添加在js代码中,让程序停下来等待输入,方便使用gdb断下进行调试。该方法比写一个while死循环好在,让程序停下后,还可以让程序继续运行下去。 |
V8-gdb:
v8
里面也自带了gdb
的一些调试命令,在tools
目录下有两个脚本:
1 | gdb-v8-support.py |
source
到gdb
中即可。
常用的有:
1 | job //完整打印v8对象的结构体 |
v8 leak and getshell:
泄漏先提三种方法,前两种是靠泄漏libc
来做,但是最后最后一种就没有libc
的要求了。
看脸leak:
这种不稳定性还是比较大的,先随便new
一个:
1 | var test = [1,2,3,4.4]; |
看内存位置:
1 | pwndbg> x/10xg 0x15608e98dde1-1 |
在他上方的很远的位置上:
1 | pwndbg> telescope 0x15608e98dde1-1-6000 100 |
1 | pwndbg> x/2xg 0x555555eef7b0 |
可以看到0x15608e98c720
和0x15608e98c730
处的地址就是d8
中的指令地址:
1 | pwndbg> vmmap 0x555555eef7b0 |
所以我们就可以根据数组对象的地址往上遍历寻找,尽管开了aslr
,但是后三个bit
是不会变的,所以我们只要寻找地址内容的后三个bit
是7b0
,且里面的机器码是e9ff73381e358d48
即可leak
出d8
的程序基址。
leak
出程序基地址后只需要找到got
表,读取got
表中的函数真实地址即可leak libc
基址。之后就是常规的改__free_hook
为one_gadget
或者system
了。
如果改为system
那么只需要再申请一块ArrayBuffer
并且写入/bin/sh
即可,等待GC
的回收调用free
。
但是这个看脸leak
有一个缺点就是上面所得到的0x555555eef7b0
程序地址会改变,一定的时间后会变成另外的内容。而且不同环境下也会不同,所以非常的不稳定。
稳定leak:
接下来的这一种就是很稳定的leak
方式,但是总的流程跟上面还是一致的。
查询流程为:
1 | JSArray --> <map> --> <map>.constructor --> code |
JSArray:
1 | pwndbg> x/10xg 0x094573e4dde1-1 |
map —> *(JSArray+0x0):
1 | pwndbg> x/10xg 0x0000282b4ab02ed9-1 |
map constructor:
这里就不用去找了,直接test.constructor
即可得到:
1 | %DebugPrint(test.constructor); |
1 | pwndbg> x/10xg 0x0b1e87a10ec1-1 |
code —> *(constructor+0x30):
1 | pwndbg> telescope 0x00002fae4c986981-1 0x20 |
如上图在0x40
处就是d8
的指令地址:
1 | pwndbg> vmmap 0x5555560294e0 |
所以很容易的,后面的流程跟上面的看脸leak
也一样了。
该指令其实是用于内置数组构造所使用的,所以只要是数组就会有,能够不受影响的稳定泄漏。v8在生成一个数组对象过程中,会对应着生成一个code对象,这个code对象中存储了和该数组对象相关的构造函数指令,而这些构造函数指令又会去调用d8二进制中的指令地址来完成对数组对象的构造。
WASM劫持:
wasm
就是可以让JavaScript
直接执行高级语言生成的机器码的一种技术。
1 | WasmFiddle: https://wasdk.github.io/WasmFiddle/ |
直接将上面的代码上手测试:
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]); |
这里wasmInstance.exports.main
得到的其实是该函数的地址,该地址赋值给了f
,f()
则是执行了该函数。
1 | pwndbg> r |
这里的思路就是将wasmCode
中的可执行区域替换成构造的shellcode
即可劫持程序流。查询流程:
1 | Function –> shared_info –> WasmExportedFunctionData –> instance |
Function:
1 | pwndbg> x/10xg 0x04d6c3fdfab1-1 |
shared_info —> *(Function+0x18):
1 | pwndbg> x/10xg 0x000004d6c3fdfa79-1 |
WasmExportedFunctionData —> *(shared_info+0x8):
1 | pwndbg> x/10xg 0x000004d6c3fdfa51-1 |
instance —> *(WasmExportedFunctionData+0x10):
且地址在instance+0x88
处:
1 | pwndbg> telescope 0x000004d6c3fdf8b9-1+0x88 |
0x10693176a000
就在一个可读可写的区域,所以可以得到该地址并往上面写入shellcode
:
1 | pwndbg> vmmap 0x10693176a000 |
最终写完shellcode
后只需要执行:
f();
即可调用wasm
的函数接口,触发shellcode
。
原理、注意点:
wasm内存页存储的是wasm函数最后能够调用的汇编代码,shellcode覆盖到这里,后续会执行到,起初认为是shellcode覆盖的是wasm字节码;实际上wasm字节码只是一段AST字节码,会由v8解析执行而已;wasm函数对象中关于内存页的偏移不一定是+0×88,有可能要根据具体编译的v8程序进行调整。
write原语:
这里说一下任意写的原语。
有了任意写的原语之后会遇到写入crash
的问题,那是因为写入的值以7f
开头的缘故,float
对7f
开头会有一定处理的缘故,所以需要改写一下write
原语。
这里改写只需要改写ArrayBuffer
的backing_store
字段就行,因为往ArrayBuffer
中写的时候是存储在backing_store
字段的,所以只要把backing_store
指向的地址改成我们所需要改写地方的地址即可。
1 | var buff = new ArrayBuffer(0x10); |
1 | pwndbg> x/10xg 0x0ea2f8d8dde1-1 |
可以看见0x55555639fe10
里存储的就是我们所输入的0x12345678
。
所以很简单的,先用任意写原语将backing_store
字段改为我们所需要修改点的地址,再往dataview
中写入数据即可。
杂:
0x12345678n
后的n
代表大整数,即Bigint
。
DataView.setFloat64(0,0x100,true)
,0
代表起始字节,true
代表小端序,默认false
大端序。
Chromium环境搭建:
首先装好depot_tools
然后就可以跟着官方文档走一波了。当然常规的,代理要好。
mkdir ~/chromium && cd ~/chromium
fetch --nohooks chromium
//这里千万不要加 –no-history//因为这是把commit提交记录全部都删掉了,后面用不了checkout回退版本
cd src
./build/install-build-deps.sh
//一般就会加上 –no-chromeos-fonts ,因为这玩意儿老下载不下来。。//这里主要是下载后面ninja编译需要用到的一些工具,避免后面编译报错
gclient runhooks
//下载额外的二进制文件和一些后面编译需要用到的东西,跟上者差不多。gn gen out/Default
//创建编译链接,跟v8编译的时候差不多。autoninja -C out/Default chrome
//等ninja编译,大概要一个下午的时间(可能还不够。out/Default/chrome
//可以愉快的开始玩弄chromium了。
后面是需要checkout的过程:
如果需要用到旧的hash版本来调一些cve的话,那就是在上面的3和4步之间插入以下语句即可。
git checkout 2bd7464ec1efc9eb24a38f7400119a5f2257f6e6
//hashgclient sync
//下载原hash文件
开始fetch chromium
的时候如果中断了也可以使用来从之前恢复中断:
gclient sync