Level 8B - Super "Optimised" Swap (SpiderMonkey pwn)

This is a JavaScript engine exploit. More specifically, we are given a binary called js, which is the SpiderMonkey JavaScript engine. SpiderMonkey is Mozilla's JavaScript and WebAssembly Engine, used in Firefox, Servo and various other projects. It is written in C++, Rust and JavaScript.

Usually for JS pwn challenges, vulnerable patches are made to the JS engines, and you need to write an exploit in JS to gain an RCE over the remote server. As this was my first time writing a JS exploit, I had to do a ton of research in order to understand what was going on ๐Ÿ˜ญ. On the bright side it meant that I learnt alot more about memory management in the SpiderMonkey engine :).

How do I build my own SpiderMonkey JS shell with debug symbols?

https://wiki.mozilla.org/JavaScript:New_to_SpiderMonkey#Build_the_js_shell This isn't necessary to solve the challenge, and I actually didn't do it. It was unnecessary since the author already provided multiple helpful debug messages that print the addresses of the elements and the values getting swapped.

The challenge source is mostly in code/diff.txt, which shows us how the JS engine was modified to make it vulnerable. Let's zoom in on the important parts:

The idea is that arrays have a new .swap() function introduced, and its intended functionality is to swap elements in an array. Let's break down what it does:

  1. Gets the len of the array using GetLengthPropertyInlined

  2. Checks that the array can be optimised for dense storage

  3. Get the array capacity using getDenseCapacity()

  4. Get the underlying js::Value* elements by using nobj->getDenseElements()

  5. Get the from and to properties of the array using GetProperty() . These are both 32-bit integers.

  6. Check that the indices from and to are both < len and < capacity

  7. Actually perform the swap on the js::Value members using memcpy

Using LLMs, I learnt that the GetProperty may invoke arbitrary attacker-controlled JavaScript. In other words, when GetProperty is called to retrieve the index to or from, we can perform operations on the array before returning the integer index.

Having done numerous heap-based userspace pwn challenges before, my first observation was that the elements of the array are stored in js::Value* elements2 before we call GetProperty().

This means that if we can find a way to free the array's backing buffer and reclaim it using other objects, we could potentially achieve an arbitrary swap between elements/metadata of two objects. My guess (which was correct) was that expanding the array beyond the capacity would cause it to be allocated somewhere else.

I also came across SpiderMonkey blogs which mentioned the objectAddress() function which was directly accessible from the JavaScript engine, which saved me the effort of having to implement one using the existing primitives.

Furthermore, on reading previous blogs/CTF challenges, I realised that a popular way of exploiting SpiderMonkey was to first obtain arbitrary read and write, then use those primitives to:

  1. Write a function containing shellcode encoded in doubles, and execute it multiple times so the shellcode is mapped into RX memory

  2. Read the JsJitInfoAddr which is located at objectAddress(shellcode) + 0x28

  3. Read the first 8 bytes in JsJitInfoAddr to leak the memory address of the readable and executable region (and hence have a better idea of where existing user shellcode is located)

  4. Write the address of user shellcode into jitInfoAddr so it gets executed by the JIT engine

Where can I find more resources on this exploit technique?

https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/

Note that this blog is in Windows, but many concepts are still applicable to Linux. A key difference between my exploit and theirs is that they find the RX shellcode address by scanning for a magic byte. In my case I needed to manually tweak values for every single arbitrary read, which made this difficult. I had the advantage of having a consistent exploit environment, so I could calculate the offset of the RX shellcode from the RX region, thus I can skip the stage of scanning for shellcode magic bytes to determine where the RX shellcode is.

https://www.sentinelone.com/labs/firefox-jit-use-after-frees-exploiting-cve-2020-26950/#vuln

This blog pretty much does the same thing in linux and has diagrams

As I was unfamiliar with V8/SpiderMonkey exploits, I also researched on various existing ways of exploiting UAF on arrays. After a few days, I encountered the following holy grail when reading this sentinelone blog:

This prompted me to look at how ArrayBuffers and Arrays were stored in memory. As an exercise, let me run you through how I get an arbitrary read. Suppose I have a function shellcode and its object address. How do I read the content of objectAddress(shellcode) + 0x28? I need this arbitrary read to determine the address of JSJitInfoAddr.

Here's what I'm doing:

  1. Use Object.defineProperty(arr1, 'to', ...) to dictate the JS that is run before the to index is returned to the JS engine. When getProperty() is called, we will:

    1. Expand the array arr1 from 64 -> 65 elements, expanding it beyond its capacity to trigger a re-allocation elsewhere.

    2. Perform garbage collection to free its previous backing buffer.

    3. Create a certain number of typed BigUint64Arrays such that part of arr1's previous backing buffer gets reclaimed

    4. Create another array arr2 whose only element is the decimal representation of objectAddress(shellcode) + 0x28

    5. Finally return the index to and return control to the JS engine

  2. Allow the swap to occur, swapping the data pointer of the LAST BigUint64Array with the VALUE objectAddress(shellcode) + 0x28

  3. Now when we read the first element of the last BigUint64Array, we will effectively be dereferencing objectAddress(shellcode) + 0x28 and reading the 8 bytes at its address.

How do you debug the exploit running in the JavaScript engine?

My workflow was to get the arbread and arbwrite working in my host, before testing it (and making the necessary adjustments) on the docker container. I had to install pwndbg on my docker container for this :(

In our case, the exploit is loaded from a file by doing p = process(["./js", f"--file={myfile}"]) . As such, to debug you would do something like gdb --args ./js -f solve.js.

Then set a breakpoint like break math_pow.

Within your javascript exploit, when you want execution to pause add a function call like Math.pow(10,2). You're now good to go!

In this run objectAddress(shellcode) = 0xf4ad676c158

If we run the arbread function we'll see the following debug message from the JS engine:

This tells us that the arr1's elements were at the address 0xd643eb00cb8 and we can look there to check what reclaimed it. Notice that I swapped index 11 (0x0b) and 34 (0x22) here.

This is the memory layout in the debugger AFTER the swap has occurred:

As you can see, I have swapped the elements at 0xd643eb00d10 and 0xd643eb00dc8 . This swaps the last BigUint64Array's data pointer with objectAddress(shellcode) + 0x28.

Now, when I try to read the 0th element of the BigUint64Array, it will dereference objectAddress(shellcode) + 0x28 and give me that value instead of the actual BigUint I put in it! (which was 0x42424242)

This first arbitrary read allows me to read the contents of objectAddress(shellcode) + 0x28 = address of JSJitInfoAddr. I use another similar arbitrary read to read the content of JSJitInfoAddr, which is the virtual address of the RX region.

Now we come to the arbitrary write. In order for the shellcode to be executed by the JIT engine, we have to find the address of the executable shellcode and overwrite the value at JSJitInfoAddr with that. Since the environment was fixed, I found out that the executable shellcode would be assigned at a constant offset with respect to the RX region that we already leaked. Therefore, we just need to overwrite the value of JSJitInfoAddr with that address to successfully pwn it.

Here's a code snippet for the arbitrary write:

The main idea is essentially the same: I allocate a bunch of BigUint64Arrays to partially reclaim the backing buffer of arr1, then allocate arr2 containing the target address in decimal. Then I swap the data pointer of the last BigUint64Array with the actual addr element.

After the swap is performed, when we try to write element 0 of the last BigUint64Array, we will end up dereferencing addr and changing the value in it, achieving our arbitrary write.

This allows us to successfully overwrite the first 8 bytes of JSJitInfoAddr to the address of the executable shellcode. Finally, to activate the RCE, we need to simply invoke the shellcode() function. Below is the full exploit:

Last updated