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!
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);
}
}
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);
}
}
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;