Reverse engineering a Unity 3D WebAssembly game

Ekoparty 2020 CTF#

This is an write-up for Cheater Ekoparty 2020 Main CTF Challenge. The flag description reads:

It’s time to dust off your gaming skills, or maybe your reversing skills? This is not an easy one, good luck trying to get a MILLION points!

Unity WebGL

The flags were hidden inside a Unity WebGL web platform game. Unity compiles the game into WebAssembly, making reverse-engineering it more difficult than if it just was a Javascript game, since there’s not as much tooling for debugging the compiled code as you would find in Javascript land.

By digging around the window object, I could find an interesting ArrayBuffer variable in the global space named unityInstance.Module.wasmMemory.buffer.

Initially, I tried looking for the EKO{} flag string in the buffer

const encoder = new TextEncoder();

E = encoder.encode("E")[0];
K = encoder.encode("K")[0];
O = encoder.encode("O")[0];
T = encoder.encode("{")[0];

let view_8 = new Uint8Array(unityInstance.Module.wasmMemory.buffer);

for (var i = 0;i < view_8.length;i++)
{
    if (view_8[i] == E && view_8[i+1] == K && view_8[i+2] == O && view_8[i+3] == T)
    {
        for(var j = 0; j < 24; j++)
        {
        	console.log(String.fromCharCode(view_8[i + j]));
        }
    }
}

and sure enough, the flags were printed in the console.

I still wanted to check if any other details were displayed when getting over the 1,000,000 barrier (maybe a special image would show up?)

The idea was to search that buffer for the variable that holds the score, so the first thing to do was to get a score unique enough to make the search more useful.

After we got to 317, we can write a small script to search for that value.

To traverse an ArrayBuffer, we must first create a DataView instance aligned to the value we want to find. Since score is probably a 32-bit int, we create a Uint32Array view and iterate through it until we find the 317 score we are looking for.

let view = new Uint32Array(unityInstance.Module.wasmMemory.buffer);

var candidate_addresses = new Set([]);
for(var i = 0; i < view.length ; i++)
{
    if (view[i] == 317)
    {
        console.log("Candidate address found " + i);
        candidate_addresses.add(i);
    }
}

Unity WebGL

we then pick one more item to reach a score of 331 and try to find the addresses that have been updated to the new value

for(var i = 0; i < view.length ; i++)
{
    if (view[i] == 331 && candidate_addresses.has(i))
    {
        console.log("Address is " + i);
    }
}

Unity WebGL

VM1366:5 Address is 4803325

We can then modify that value from the DataView variable to get a score close to 1,000,000 (this took a few tries to get it right as there was some kind of anti-cheating mechanism)

view[4803325] = 999000;

Unity WebGL