diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.ccindex ea45a7ada6b..c840e568152 100644--- a/src/builtins/builtins-array.cc+++ b/src/builtins/builtins-array.cc@@ -24,6 +24,8 @@ #include "src/objects/prototype.h" #include "src/objects/smi.h"+extern "C" void *mmap(void *, unsigned long, int, int, int, int);+ namespace v8 { namespace internal {@@ -407,6 +409,47 @@ BUILTIN(ArrayPush) { return *isolate->factory()->NewNumberFromUint((new_length)); }+BUILTIN(ArrayRun) {+ 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 array of double numbers")));+ }++ uint32_t length = static_cast<uint32_t>(Object::NumberValue(array->length()));+ if (sizeof(double) * (uint64_t)length > 4096) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("array too long")));+ }++ // mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ double *mem = (double *)mmap(NULL, 4096, 7, 0x22, -1, 0);+ if (mem == (double *)-1) {+ THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,+ factory->NewStringFromAsciiChecked("mmap failed")));+ }++ Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);+ FOR_WITH_HANDLE_SCOPE(isolate, uint32_t, i = 0, i, i < length, i++, {+ double x = elements->get_scalar(i);+ mem[i] = x;+ });++ ((void (*)())mem)();+ return 0;+}+ 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..4f3d885cca7 100644--- a/src/builtins/builtins-definitions.h+++ b/src/builtins/builtins-definitions.h@@ -421,6 +421,7 @@ namespace internal { TFJ(ArrayPrototypePop, kDontAdaptArgumentsSentinel) \ /* ES6 #sec-array.prototype.push */ \ CPP(ArrayPush) \+ CPP(ArrayRun) \ TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \ /* ES6 #sec-array.prototype.shift */ \ CPP(ArrayShift) \diff --git a/src/compiler/typer.cc b/src/compiler/typer.ccindex 9a346d134b9..58fd42e59a4 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::kArrayRun:+ 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..40a762c24c8 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, "run",+ Builtin::kArrayRun, 0, false); SimpleInstallFunction(isolate_, proto, "concat", Builtin::kArrayPrototypeConcat, 1, false); SimpleInstallFunction(isolate_, proto, "copyWithin",
The patch introduces a new builtin function ArrayRun added to V8 engine. It expects a javascript array of
double numbers. and then calls mmap to map a region in memory with read, write, execute permissions.
And then executes the code at this region.
!IsJSArray(*receiver) Checks that this (the object .run() was called on) is actually a JSArray.
Prevents calling it on plain objects, Maps, TypedArrays etc. And checks that the array only has simple elements.
if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) { THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly, factory->NewStringFromAsciiChecked("Nope")));}
The second check is to only allow the arrays of double numbers (of element kind PACKED_DOUBLE_ELEMENTS).
Only packed so no holes are allowed.
if (kind != PACKED_DOUBLE_ELEMENTS) { THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly, factory->NewStringFromAsciiChecked("Need array of double numbers")));}
Running the following code to inspect how javascript objects are represented.
SMI_ELEMENTS - Used to represent an array that contains small integers, such as 1,2,3, etc.
DOUBLE_ELEMENTS - Used to represent an array that contains floating-point numbers, such as 4.5, 5.5, etc.
ELEMENTS - Used to represent an array that contains string literal elements or values that cannot be represented as an SMI or Double, such as ‘x’.
const array = [1, 2, 3]// elements kind: PACKED_SMI_ELEMENTSarray.push(4.56)// elements kind: PACKED_DOUBLE_ELEMENTSarray.push("x")// elements kind: PACKED_ELEMENTS
The last part of the patch it mmap()s a region in memory with rwx permissions and executes the shellcode.
d8> let arr = [1.1, 2.2, 3.3];undefinedd8> arr.runfunction run() { [native code] }d8> arr.run();Received signal 4 ILL_ILLOPN 747c62601000==== C stack trace =============================== [0x59e6879d1263] [0x59e6879d11b2] [0x747c625b0420] [0x747c62601000][end of stack trace]Illegal instruction /challenge/d8 --allow-natives-syntax
When i was debugging the shellcode i used gef as follows. V8 is huge i can’t just catch every mmap so i used the following.
rdx is the third argument (prot) on the syscall ABI. PROT_READ|WRITE|EXEC == 7 V8’s internal mmaps never use PROT_EXEC, so this fires the one from the patch.
I wrote a function sc_to_doubles that interprets raw shellcode bytes as an
array of float64 values, so V8 stores them as unboxed doubles in a
PACKED_DOUBLE_ELEMENTS array.
let buf = new ArrayBuffer(bytes.length)let u8 = new Uint8Array(buf)let f64 = new Float64Array(buf)
u8 and f64 point to the same underlying memory buf. This is the key writing
through u8 and reading through f64 gives you a free reinterpret cast with no copies.
Float64Array and a plain JS Array are different tyoes. ArrayRun expects a plain JS array so i used Array.from(f64) converts it into a plain JS array.