前言:
大概是入门级别的一次分析(或者入门都不算,很简单的一个分析orz。
首发于先知社区。
正文:
拿到一个diff
:
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
看新加的oob
函数就行(虽然我也看不太懂写的是个啥玩楞2333。里面的read
和write
注释,还有直接取了length
可以大概意识到是一个越界读写的漏洞。
a.oob()
就是将越界的首个8字节给读出,a.oob(1)
就是将1
写入越界的首个8字节。
那么越界读写就好办了,先测试一下看看:
1 | ➜ x64.release git:(6dc88c1) ✗ ./d8 |
因为v8中的数以浮点数的形式显示,所以先写好浮点数与整数间的转化原语函数:
1 | var buff_area = new ArrayBuffer(0x10); |
上手调试,先看看一个数组的排布情况:
1 | var a = [0x1000000,2,3,4]; |
1 | pwndbg> x/10xg 0x101d1f8d0069-1 |
所以此时的a.oob()
所泄漏的应该是0x000012a265ac0851
的double形式。但是我们无法知道0x000012a265ac0851
是什么内容,不可控。那么我们换一个数组,看以下数组情况:
1 | var a = [1.1,2.2,3.3,4.4]; |
1 | pwndbg> x/10xg 0x0797a34100c9-1 |
我们可以看见FixedArray
和JSArray
是紧邻的,所以a.oob()
泄漏的是0x00001c07e15c2ed9
,即JSArray
的map
值(PACKED_DOUBLE_ELEMENTS
)。这样我们就好构造利用了。
类型混淆:
假设我们有一个浮点型的数组和一个对象数组,我们先用上面所说的a.oob()
泄漏各自的map
值,在用我们的可写功能,将浮点型数组的map
写入对象数组的map
,这样对象数组中所存储的对象地址就被当作了浮点值,因此我们可以泄漏任意对象的地址。
相同的,将对象数组的map
写入浮点型数组的map
,那么浮点型数组中所存储的浮点值就会被当作对象地址来看待,所以我们可以构造任意地址的对象。
泄漏对象地址和构造地址对象:
先得到两个类型的map
:
1 | var obj = {"A":0x100}; |
再写出泄漏和构造的两个函数:
1 | function leak_obj(obj_in){ //泄漏对象地址 |
得到了以上的泄漏和构造之后我们想办法将利用链扩大,构造出任意读写的功能。
任意写:
先构造一个浮点型数组:
1 | var test = [7.7,1.1,1,0xfffffff]; |
再泄漏该数组地址:
1 | leak_obj(test); |
这样我们可以得到数组的内存地址,此时数组中的情况:
1 | pwndbg> x/20xg 0x2d767fbd0019-1-0x30 |
我们可以利用构造地址对象把0x2d767fbcfff8
处伪造为一个JSArray
对象,我们将test[0]
写为浮点型数组的map
即可。这样,0x2d767fbcfff8-0x2d767fbd0018
的32字节就是JSArray
,我们再在0x2d767fbd0008
任意写一个地址,我们就能达到任意写的目的。比如我们将他写为0x2d767fbcffc8
,那么0x2d767fbcffc8
处就是伪造的FixedArray
,0x2d767fbcffc8+0x10
处就为elements
的内容,把伪造的对象记为fake_js
,那么执行:
1 | fake_js[0] = 0x100; |
即把0x100复制给0x2d767fbcffc8+0x10
处。
任意读:
任意读就很简单了,就是:
1 | console.log(fake_js[0]); |
取出数组内容即可。
那么接下来写构造出来的任意读写函数:
1 | function write_all(read_addr,read_data){ |
有了任意读写之后就好利用了,可以用pwn
中的常规思路来后续利用:
- 泄漏libc基址
- 覆写
__free_hook
- 触发
__free_hook
后续在覆写__free_hook
的过程中,会发现覆写不成功(说是浮点数组处理7f
高地址的时候会有变换。
所以这里需要改写一下任意写,这里我们就需要利用ArrayBuffer
的backing_store
去利用任意写:
先新建一块写区域:
1 | var buff_new = new ArrayBuffer(0x20); |
这时候写入:
1 | dataview.setBigUint64(0,0x12345678,true); |
在ArrayBuffer
中的backing_store
字段中会发现:
1 | pwndbg> x/10xg 0x029ce8f500a9-1 |
因此,只要我们先将backing_store
改写为我们所想要写的地址,再利用dataview的写入功能即可完成任意写:
1 | function write_dataview(fake_addr,fake_data){ |
而后就可以按照正常流程来读写利用了。
这里就介绍一种在浏览器中比较稳定利用的一个方式,利用wasm
来劫持程序流。
wasm劫持程序流:
在v8
中,可以直接执行wasm
中的字节码。有一个网站可以在线将C语言直接转换为wasm并生成JS调用代码:https://wasdk.github.io/WasmFiddle
。
左侧是c语言,右侧是js
代码,选Code Buffer
模式,点build
编译,左下角生成的就是wasm code
。
有限的是c语言部分只能写一些很简单的return
功能。多了赋值等操作就会报错。但是也足够了。
将上面生成的代码测试一下:
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]); |
会得到42
的结果,那么我们很容易就能想到,如果用任意写的功能,将wasm
中的可执行区域写入shellcode
呢?
我们需要找到可执行区域的字段。
直接给出字段:
1 | Function–>shared_info–>WasmExportedFunctionData–>instance |
在空间中的显示:
1 | Function: |
1 | shared_info: |
1 | WasmExportedFunctionData: |
1 | instance+0x88: |
1 | pwndbg> vmmap 0x27860927e000 |
可得知0x144056c21dc0
处的0x27860927e000
为可执行区域,那么只需要将0x144056c21dc0
处的内容读取出来,在将shellcode
写入读取出来的地址处即可完成程序流劫持:
1 | var data1 = read_all(leak_f+0x18n); |
利用成功:
EXP:
1 | var buff_area = new ArrayBuffer(0x10); |