V8从0开始
一、环境搭建
参照[原创]v8利用初探 2019 StarCTF oob 复现分析-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
1.代理设置
1 | git config --global http.proxy http://ip:port |
2.代码下载
1 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git |
二、前置知识
参考:从一道CTF题零基础学V8漏洞利用 - FreeBuf网络安全行业门户
1.调试知识
需要注意的是debug版本的用来辅助显示,直接是没办法运行相关代码的,realse用来实际运行。
(1)工具
常见的pwndbg,可以搭配如下的小工具
源码的/pathto/v8/tools
目录下有专门用来调试v8的gdbinit,加在~/.gdbinit
中即可
1 | source /pathto/v8/tools/gdbinit |
(2)函数
在运行./d8时加入--allow-natives-syntax
选项,可以使用一些调试函数
1 | ./d8 --allow-natives-syntax |
如下函数
1 | %DebugPrint(obj) //输出对象地址 |
示例代码如下:
1 | var a = [1,2,3]; |
2.基础知识
(1)对象内存讲解
1 | var a = [1,2,3]; |
比如如下定义数组对象a,那么运行%DebugPrint(a);
得到地址后,使用job命令可以看到如下的内存布局
v8在内存中只有数字和对象两种表示。为了区分两者,v8在所有对象的内存地址末尾都加了1,以便表示它是个对象。因此上图该对象的实际内存地址应该为(0x346b7364dde9-1=0x346b7364dde8)
而对于数组对象则大致如下布局
map | 表明了一个对象的类型对象,上图即为PACKED_SMI_ELEMENTS |
---|---|
prototype | prototype |
elements | 对象元素 |
length | 元素个数 |
properties | 属性 |
而对于其中的elements元素也是一个对象,且地址位于对象a的上方,能看到里面的内容,也就是说先申请的elements这个元素内存,然后再申请的数组对象a
那么总的内存布局如下
1 | elements ----> +------------------------+ |
其他类型的对象都有些类似,可以自己去尝试看看
其中也不一定元素后面就跟的是ArrayObject
的内容,也可能中间间隔了很多东西,比如如下情形,elements
的地址和ArrayObject
的地址差的还是挺大的,可能是内存分配上的一些问题吧?不太懂v8的内存分配是怎么实现的。
(2)不同对象类型内存分布
①数组对象
1 | var array = [1.1,2.2,3.3]; |
如下内存布局
1 | elements ----> +------------------------+<---------+ |
double或者float的数组也相似,就是map的类型为PACKED_DOUBLE_ELEMENTS
②对象数组对象
1 | var objectArray = [a, b];//a,b为对象 |
即数组里存放的是对象,也是类似的,就是在elements中会有点不一样,相当于存放一个指针指向包含的对象,包括map的类型也不同,为PACKED_ELEMENTS
1 | elementA ----> +------------------------+<---------+ |
(3)类型混淆
即当我们能够修改ArrayObject
的map属性时,就能够造成类型混淆。比如
1 | let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组 |
同样的,当我们能够修改把一个浮点数数组的map属性改为对象数组的map属性时,就能够使得v8认为该浮点数数组中的浮点数实际是一个对象,而我们控制数组中的变量的值为一个地址,这样就能将一个任意地址转化为一个obj对象了。
1 | let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组 |
三、实际题目
参考:从一道CTF题零基础学V8漏洞利用 – backup (4hou.win)
1.题目分析
添加的diff如下,主要是一下两部分,其他部分加入的就只是为了正常运行而已
1 | + SimpleInstallFunction(isolate_, proto, "oob", |
即添加了为数组对象ArrayOob
添加了一个函数oob(),其功能为
- 当参数只有一个(默认传入this指针),返回数组最后一个元素之后的元素
- 当参数有两个(除了this指针之外再传入一个参数),就用我们传入的参数覆盖数组最后一个元素之后的元素
- 其他情况下返回一个undefined
即该函数可以实现读/写数组对象MAP属性
2.漏洞分析
关键在于数组对象ArrayOob
的内存空间,尝试看一下
1 | let float_array = [1.1,2.2,3.3,4.4]; |
调试跑起来,查看elements处的内存
即在数组对象的元素之后为数组对象的结构体,之前也提到过。而对象的map如果能被修改,就能引起v8的类型混淆,之前已经提到过。
那么我们利用类型混淆,来构造获取任意对象的地址的函数getAddress(obj)
以及修改任意地址为对象的函数getFakeObj(addr)
(1)构造转化函数
1 | let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组 |
实际测试一下
1 | let obj_addr = getAddress(obj); |
可以看到成功获取地址
但是这么判断转化对象成功不太知道…
(2)利用转化函数构造任意读写
得到了上述的转化函数,那么我们可以借助这两个转化函数来获取任意地址读和任意地址写
原理:
利用getFakeObj
将一个地址转化为一个浮点数组对象,而如果该地址指向的elements指针可控,那么我们就能够修改其elements指针,从而指向任意地方,去进行读写操作。
需要注意的是,由于依据elements读写的时候没有进行检测,所以只要成功修改了elements指针,那么我们我们就能从elements实际存放数据的地方,也就是*(elements+0x10)处读写我们的数据,造成任意地址读写。
假定申请fake_array[6]
,如下图布局
即如上图所示,设置fake_array_addr-0x40+0x10
的地址为一个fake_obj
,那么该fake_obj
相关的属性如下
map
即为float_array_map
,可以由fake_array[0]进行控制,由此可以进行元素读写protototype
即设置为0,可由fake_array[1]
进行控制elements
即设置为我们需要读写的地址,可由fake_array[2]
进行控制
那么即可完成整个读写布局。
代码:
1 | let fake_array = [ |
测试效果:
1 | %DebugPrint(fake_array); |
那么我们即读写fake_array[3]处的值,可以看到成功读写
3.实际利用
(1)常规堆思路
通过泄露d8的ELF地址,计算其GOT表地址,任意读泄露出libc地址,然后覆盖free_hook
为system
函数,之后通过console.log('/bin/sh\x00')
即可释放一个包含/bin/sh
的堆块完成利用
这个泄露地址部分有点不太好搞,随机泄露的部分没看,稳定泄露的部分不太对,获取到的不是d8中的指令地址,而是一个lib库的指令地址。
🔺后面补把
(2)利用WASM机制
如下可以将C语言直接转换为wasm并生成JS配套调用代码
wasm就是一个用来调用C代码的机制,可以自己去看看,但是不能赋予危险的代码来调用,只能运行数学计算、图像处理等系统无关的高级语言代码。
简单来说,我们可以创建一片WASM的空间,然后如果我们可以修改到这片运行WASM代码的内存空间(利用上述的任意写),修改其为shellcode,然后当d8调用WASM的接口时,就可以调用到我们的shellcode了。
而运行WASM代码的内存空间即为WASM_instance+0x88处
1 | let 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
地址+0x88处
其中wasmCode的功能为
1 | int main() { |
①泄露地址
1 | let wasm_instance_addr = getAddress(wasmInstance); |
②写入shellcode到wasm的rwx段
这里有个问题,就是连续使用两次write64
会出错,具体原因不清楚,说什么floatArray已经被篡改,再检测合法性会出错,被篡改成啥也不知道啊,这边有点问题,之后看能不能补上。
🔺
那么依据大佬的,需要进行修改,使用dataview
来进行修改
原理即为
DataView对象中的backing_store
会指向申请的data_buf
(backing_store
相当于我们的elements),修改backing_store
为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。
🔺
不太懂
1 | function writeAny(addr,data) |
③调用wasm来执行shellcode
1 | f(); |
直接通过这个调用即可。
EXP:
1 | function hex(i) |
starctf2019/pwn-OOB at master · sixstars/starctf2019 (github.com)