概述
在v8利用上,我觉得也有一个明确的目标,就是执行任意shellcode
。当有了这个目标后,下一步就是思考,怎么写shellcode
呢?那么就需要有写内存相关的洞,能写到可读可写可执行的内存段,最好是能任意地址写。配套的还需要有任意读,因为需要知道rwx内存段的地址。就算没有任意读,也需要有办法能把改地址泄漏出来(V8的binary保护基本是全开的)。接下来就是需要能控制RIP,能让RIP跳转到shellcode
的内存段
WASM
现如今的浏览器基本都支持WASM,v8会专门生成一段rwx内存供WASM使用,这就给了我们利用的机会。测试代码如下:
1 | %SystemBreak(); |
然后使用gdb进行调试,在第一个断点的时候,使用vmmap
来查看一下内存段,这个时候内存中是不存在可读可写可执行的内存断的,我们让程序继续运行。
在第二个断点的时候,我们再运行一次vmmap
来查看内存段:
1 | pwndbg> vmmap |
因为WASM代码的创建,内存中出现可rwx的内存段。接下来的问题就是,我们怎么获取到这个地址呢?首先我们来看看变量f
的信息:
1 | DebugPrint: 0x19a4081d370d: [Function] in OldSpace |
可以发现这是一个函数对象,我们来查看一下f
的shared_info
结构的信息:
1 | pwndbg> job 0x19a4081d36e9 |
接下里再查看其data
结构:
1 | pwndbg> job 0x19a4081d36bd |
再查看instance
结构:
1 | pwndbg> job 0x19a4081d35b9 |
仔细查看能发现,instance
结构就是js代码中的wasmInstance
变量的地址,在代码中我们加入了%DebugPrint(wasmInstance);
,所以也会输出该结构的信息,可以去对照看看。
我们再来查看这个结构的内存布局:
仔细看,能发现,rwx段的起始地址储存在instance+0x60
的位置,不过这个不用记,不同版本,这个偏移值可能会有差距,可以在写exp的时候通过上述调试的方式进行查找。
根据WASM的特性,我们的目的可以更细化了,现在我们的目的变为了把shellcode
写到WASM的代码段,然后执行WASM函数,那么就能执行shellcode
了。
数据存储结构
首先来看看JavaScript的两种类型的变量的结构:
1 | a = [2.1]; |
对照变量a的job结果和内存内容如图,我们可以推断出a的数据存储结构
1 | | 32 bit map addr | 32 bit properties addr | 32 bit elements addr | 32 bit length| |
可以注意到只存储了低32位的地址,这是因为在当前版本的v8中,对地址进行了压缩,因为高32bit地址的值是一样的。
同理分析elements
的结构,可以得到大致
1 | | 32 bit map addr | 32 bit length | value ...... |
接下来看看变量b和c
1 | DebugPrint: 0x1f8608049991: [JS_OBJECT_TYPE] |
注意到 map 结构体中存在一项成员用以标注 elements 类型:
1 | - elements kind: PACKED_ELEMENTS |
因此可以直接将一个变量的 map 地址赋给另外一个变量,使得在读取值时错误解析数据类型,也就是所谓的“类型混淆”。类型混淆是有可能造成地址泄露的,可以考虑这样的代码:
1 | float_arr= [2.1]; |
正常访问obj_arr[0]
会得到一个对象,但如果修改obj_arr
的map
为float_arr
的map
,就会认为obj_arr
是一个浮点数数组,那么此时访问obj_arr[0]
就会得到对象float_arr
的地址了
数据读取
addressOf
同上所述,我们将这种类型混淆的读取地址方法称之为addressOf,一般写法如下:
1 | //获取某个变量的地址 |
fakeObject
与 addressOf
的步骤相反,将 float_arr
的 map
改为 obj_arr 的 map
,使得在访问 float_arr[0]
时得到一个以 float_arr[0]
地址为起始的对象
1 | //将某个地址转换为对象 |
任意地址读
可以尝试构造出这样一个结构:
1 | var fake_array=[double_array_map,int_to_float(0x4141414141414141n)]; |
其在内存中的布局应为:
1 | | 32bit elements map | 32bit length | 64bit double_array_map | 64bit 0x4141414141414141 |element |
接下来通过 addressOf 获取 fake_array 的地址,然后就能够计算出 double_array_map 的地址;再通过 fakeObject 将这个地址伪造成一个对象数组,对比下面的内存布局:
1 | | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |JSArray |
此处的 fake_array[0] 成为了 JSArray 的 map 和 properties ,fake_array[1] 被当作了 elements addr 和 length,通过修改 fake_array[1] 就能够使该 elements 指向任意地址,再访问 fakeObject[0] 即可读取该地址处的数据了(此处 double_array_map 需要对应为一个 double 数组的 map)
代码逻辑大致如下:
1 | var fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4 |
任意地址写
同上一小节一样,只需要将最后的 return 修改为写入即可:
1 | var fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4 |
shellcode写入
设置的 elements 地址为 addr-8n+1n,我们想要写 shellcode 的地址一般都是内存段在开头,那么更前面的内存空间则是未开辟的,写入时会因为访问未开辟的内存空间发生异常。因此直接性的写入不太能够成功,但间接性的方法或许还是存在的,如果向某个对象中写入数据不需要经过 map 和 length,或许就能够顺利完成了。
见如下代码
1 | var data_buf = new ArrayBuffer(0x10); |
输出
1 | DebugPrint: 0x16eb080499b9: [JSArrayBuffer] |
可以注意到JSDataView
的buffer
指向了JSArrayBuffer
,而JSArrayBuffer
的 backing_store
则指向了实际的数据储存地址,那么如果我们能够写backing_store
为 shellcode
内存段,就可以通过JSDataView
的setFloat64
方法直接写入了
其他
shellcode
1 | //Linux x64 |
其他函数
1 |
|