这是在A guided tour through Chrome's javascript compiler上的几个cve之一,为了学习v8的相关研究,将这三者一个一个攻破,下面是对应的commit。
环境搭建
用v8 action
(星阑科技的开源项目,“星阑科技”公众号上有对应文章说明)
cd v8
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
cd ..
漏洞分析
看下diff
diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc
index 94b018c..5ed3f74 100644
--- a/src/compiler/js-operator.cc
+++ b/src/compiler/js-operator.cc
@@ -622,7 +622,7 @@
V(CreateKeyValueArray, Operator::kEliminatable, 2, 1) \
V(CreatePromise, Operator::kEliminatable, 0, 1) \
V(CreateTypedArray, Operator::kNoProperties, 5, 1) \
- V(CreateObject, Operator::kNoWrite, 1, 1) \
+ V(CreateObject, Operator::kNoProperties, 1, 1) \
V(ObjectIsArray, Operator::kNoProperties, 1, 1) \
V(HasProperty, Operator::kNoProperties, 2, 1) \
V(HasInPrototypeChain, Operator::kNoProperties, 2, 1) \
显然是kNoWrite
引起的错误,我们看看这是什么意思,以下[来自]
意思就是在CreateObject
操作中无视side-effect
,经Object.create(0)
后,o
的map从fast mode
变为dictionary mode
,对于map的类型,dictionary mode
类似于hash
表存储,结构较复杂,fast mode
是简单的结构体模式。
这改动会使得原本分开保存的各个属性在Object.create()
后会被整理到一个大的hash表结构里。但是由于漏洞点,d8没有意识到存储方式的变化,也就是map模式的改变,猜测如果有side-effect
的话是会反过来影响到原来的对象的,但是显然无视这个边后,造成了还是按原来的模式存取的情况,也就导致了按原偏移存取,最终泄露内存。
poc
function bad_create(x){
x.a;
Object.create(x);
return x.b;
}
for (let i = 0;i 10000; i++){
let x = {a : 0x1234};
x.b = 0x5678;
let res = bad_create(x);
if( res != 0x5678){
console.log(i);
console.log("CVE-2018-17463 exists in the d8");
break;
}
}
源码分析
经turbolizer观察发现在generic lowering
阶段会由JSCreateObject
变为Call
,所以定位源码在对应阶段。
简单提一句turbolizer使用方法,首先在运行poc时加上--trace-turbo
然后运行完会在当目录下生成一些文件,
打开https://v8.github.io/tools/v7.1/turbolizer/index.html
根据版本号的不同修改url
中的vx.x
点击右上角的3,会提示你选择文件,就选择生成的json文件,可能不止一个,选一个打开就能看到这个页面,按1是重新给结点排序,按2是把所有结点显示出来,选择优化阶段的部分我就不说了,看前面对比的两图画的红框就知道。
//src/compiler/js-generic-lowering.cc:404
void JSGenericLowering::LowerJSCreateObject(Node* node) {
CallDescriptor::Flags flags = FrameStateFlagForCall(node);
Callable callable = Builtins::CallableFor( //======这里
isolate(), Builtins::kCreateObjectWithoutProperties);
ReplaceWithStubCall(node, callable, flags);
}
这里把JSCreateObject
用Builtins::kCreateObjectWithoutProperties
代替,转过去。
//src/builtins/builtins-object-gen.cc:1101
TF_BUILTIN(CreateObjectWithoutProperties, ObjectBuiltinsAssembler) {
Node* const prototype = Parameter(Descriptor::kPrototypeArg);
Node* const context = Parameter(Descriptor::kContext);
Node* const native_context = LoadNativeContext(context);
Label call_runtime(this, Label::kDeferred), prototype_null(this),
prototype_jsreceiver(this);
[ ... ]
BIND(
{
Comment("Call Runtime (prototype is not null/jsreceiver)");
Node* result = CallRuntime(Runtime::kObjectCreate, context, prototype, //这里
UndefinedConstant());
Return(result);
}
}
=============================================================================
//src/runtime/runtime-object.cc:316
RUNTIME_FUNCTION(Runtime_ObjectCreate) {
HandleScope scope(isolate);
HandleObject> prototype = args.at(0);
HandleObject> properties = args.at(1);
HandleJSObject> obj;
[ ... ]
// 2. Let obj be ObjectCreate(O).
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, obj, JSObject::ObjectCreate(isolate, prototype));//这里
[ ... ]
不同的prototype
属性对应不同的操作,我们直接看ObjectCreate
//src/objects.cc:1360
MaybeHandleJSObject> JSObject::ObjectCreate(Isolate* isolate,
HandleObject> prototype) {
// Generate the map with the specified {prototype} based on the Object
// function's initial map from the current native context.
// TODO(bmeurer): Use a dedicated cache for Object.create; think about
// slack tracking for Object.create.
HandleMap> map =
Map::GetObjectCreateMap(isolate, HandleHeapObject>::cast(prototype));
// Actually allocate the object.
HandleJSObject> object;
if (map->is_dictionary_map()) {
object = isolate->factory()->NewSlowJSObjectFromMap(map);
} else {
object = isolate->factory()->NewJSObjectFromMap(map);
}
return object;
}
得到原来的map,然后根据这一map类型来调用不同函数生成新的obj,看下怎么得到的map。
//src/objects.cc:5450
HandleMap> Map::GetObjectCreateMap(Isolate* isolate,
HandleHeapObject> prototype) {
[ ... ]
if (prototype->IsJSObject()) {
HandleJSObject> js_prototype = HandleJSObject>::cast(prototype);
if (!js_prototype->map()->is_prototype_map()) {
JSObject::OptimizeAsPrototype(js_prototype);//===================这里
}
[ ... ]
=====================================================================
//src/objects.cc:12518
void JSObject::OptimizeAsPrototype(HandleJSObject> object,
bool enable_setup_mode) {
if (object->IsJSGlobalObject()) return;
if (enable_setup_mode
}
[ ... ]
======================================================================
//src/objects.cc:6436
void JSObject::NormalizeProperties(HandleJSObject> object,
PropertyNormalizationMode mode,
int expected_additional_properties,
const char* reason) {
if (!object->HasFastProperties()) return;
HandleMap> map(object->map(), object->GetIsolate());
HandleMap> new_map = Map::Normalize(object->GetIsolate(), map, mode, reason);//先看这里
MigrateToMap(object, new_map, expected_additional_properties);
}
用原有的map
在Normalize
中生成新的map
,剩下的调用链。
Map::Normalize->
Map::CopyNormalized->
RawCopy->
Map::SetPrototype->
JSObject::OptimizeAsPrototype(没错,又调用了一次这个)
在Map::CopyNormalized
中
HandleMap> Map::CopyNormalized(Isolate* isolate, HandleMap> map,
PropertyNormalizationMode mode) {
int new_instance_size = map->instance_size();
if (mode == CLEAR_INOBJECT_PROPERTIES) {
new_instance_size -= map->GetInObjectProperties() * kPointerSize;
}
HandleMap> result = RawCopy(
isolate, map, new_instance_size,
mode == CLEAR_INOBJECT_PROPERTIES ? 0 : map->GetInObjectProperties());
// Clear the unused_property_fields explicitly as this field should not
// be accessed for normalized maps.
result->SetInObjectUnusedPropertyFields(0);
result->set_is_dictionary_map(true); //=================这里
result->set_is_migration_target(false);
result->set_may_have_interesting_symbols(true);
result->set_construction_counter(kNoSlackTracking);
[ ... ]
显然生成的map是dictionary
的。
利用方法
我们首先要看泄露哪里的内存。
let a = {x1 : 1, y1 : 2, z1 : 3};
a.x2 = 4;
a.y2 = 5;
a.z2 = 6;
%DebugPrint(a);
%SystemBreak();
Object.create(a);
%DebugPrint(a);
%SystemBreak();
第一次break看到数据这么存储的,对于那三个本就存在的数据,存在结构体内部,对于后来动态增加的,结构体内有一指针指向,当然结构体内部存数据的个数是有限制的,超过这一限制的也会存到properties
里。
第二次break看起来乱乱的,但是可以知道的是,没有存在结构体内部的数据了,这么说可能不太合适,知道意思就行,不理解的可以去看些讲v8对象布局的。
显然对于这个洞来说,如果通过这里达到内存泄漏,泄露的就是原来应该存在结构体内相应偏移的数据,比如本来a.x1
是对应properties+0x10
的位置,但是经Object.create(a)
后真实位置是跑到properties里了,原来的偏移不再是a.x1
。
经调试发现,此时返回的x.b
的值也不是properties+0x10
而是*properties+0x10
,properties指向的位置的0x10偏移处才是,也就是说偏移不变但是base变了,单这么说会觉得比较鸡肋,因为这样我们似乎改不了长度,但是别忘了此时存储数据的结构是一个哈希表,不是刚开始赋值的顺序,并且这个hash表的排布还会一直变化。也就是说,数据长度够长,理论上是会有原来的偏移对应着新的数据的。
比如x.a
和x.b
,本来布局是a的偏移为0x10,b为0x20,经过以上变换为hash存储后,0x10偏移处存储的变为了b,0x20偏移处变为a,当然这是理想情况,实际运用时我们可以通过将所有数据对应偏移处的数据打印出来,然后看有没有以上所说的这种数据对,如果找到了,那么显然可以使两位置存储不同类型元素,一个存对象一个存浮点数,这样addrOf
就构造出来了。
那么同样的,只要对应键值为ArrayBuffer
的backing_store
,那么我们也能直接改了,这样任意读写也有了,当然我们还需要对抗CheckMap
,因为当我们map类型变了之后,map就会发生变化,这样CheckMap
就会检测出来,所以回到头来还是要去掉Checkmap
对于优化的洞,目前接触到的多是把CheckMap
给消去,或者是构造出一个实际上可以越界的长度,而v8认为没越界的数(有关range范围的bug),这种一般就是把CheckBound
给优化掉,对于这个显然是前者
我们利用kNoWrite
来去掉Checkmap
// The given checkpoint is redundant if it is effect-wise dominated by another
// checkpoint and there is no observable write in between. For now we consider
// a linear effect chain only instead of true effect-wise dominance.
bool IsRedundantCheckpoint(Node* node) {
Node* effect = NodeProperties::GetEffectInput(node);
while (effect->op()->HasProperty(Operator::kNoWrite) //消除
effect = NodeProperties::GetEffectInput(effect);
}
return false;
}
从源码我们可以看到,两个检查节点中间的操作是kNoWrite
时,第二个检查节点可以消去。
还木有评论哦,快来抢沙发吧~