Reverse engineering an invitation

Chen Zheng Wei
5 min readDec 8, 2020

--

Over the last weekend I had the opportunity to take part in the STACK the Flags CTF organised by the GovTech Cyber Security Group. It was a particularly interesting (and draining!) experience — while my team was stuck on many of the challenges, it was a reminder that there was (and always will be) plenty more to learn about.

Another team mate (George Chen) has published write-ups here and here for the OSINT challenges, and this entry details the experience in solving the first reverse-engineering challenge, which we were all relatively new to.

At first glance the challenge looks pretty straight-forward:

Challenge description

The file provided is a zip archive containing a web page (the invitation) as well as two JavaScript files. Opening the web page in a browser reveals… a blank page. The browser’s console, however, provides the first hint of how to solve this challenge:

For obvious reasons, invite.js is obfuscated, however Firefox helpfully offers to pretty print the source, so let’s do that and see where/what the gl variable is expected to be:

Attempting to run canvas[_0x3f3a[3]](_0x3f3a[2]) results in the error that canvas is null too, so let’s take a look at what canvas is supposed to be — from the contents of the _0x3f3a array, we can tell that canvas is supposed be the DOM element with a class of G, however the canvas element on the page (intentionally) has a CSS class of only GG, so let’s modify the page source to so that the canvas elements reflects the correct class, and reload the page.

Post-reload a different error appears on the console:

Returning to the JS source to look at the declaration for gl[_0x3f3a[11]]:

Poking around at shade, ctype and cid confirms that they correspond to the attribute values of the <canvas> object on the page. Unfortunately, with little clue as to what the “correct” attribute values were supposed to be, along with time pressure during the CTF, I gave up on the strategy of attempting to get the page working “properly”. Instead, I dug deeper into the code to see what was going on:

An anonymous function here — let’s try assigning the function expression to a new variable and see if anything happens:

Somewhat readable code!

After removing the now unnecessary backslashes and executing this new chunk of code, we get an obvious attempt to frustrate analysis:

The debugger statement here is functionally equivalent to setting a breakpoint here, and will do so 1000 times — obviously not something that can be bypassed by clicking on continue in the debug console. Let’s remove the loop and rerun the code:

This is the point where it got pretty exciting. The browser didn’t show an alert, however. From a reading of the challenge description, either hhh or mmm was almost certainly to be the variable containing the flag, so let’s take a quick look at what they currently contain:

Not very useful right now, so let’s take a step back and look at what the code was doing previously. The first thing I looked at was the call to compare():

…which was probably deliberately named as such to mislead. The function actually contains XOR functionality, and the function was being called to XOR the browser’s reported hostname with the pre-defined string you're invited!!! (by character code), before comparing against the URL-decoded value. We do know, thankfully, that we can find the unknown value by XOR-ing the two known values, so let’s do that:

It does seem that we were on the right track towards determining what the script checks for, but due to the lack of time in replicating the environment that the script was looking for, this remained simply an interesting finding.

Now this is where it gets embarrassing — amidst the excitement I completely overlooked the significance of the x array, and jumped directly to the declarations for hhh and mmm. hhh was determined to be related to the hostname check and thus the focus turned to mmm, which most likely was supposed to contain the flag. After a considerable amount of wasted time attempting to bruteforce the possible return values of rrr(), it finally occurred to me that the function was returning a different value every call! Returning to the code shows us where the function was being seeded:

Now things are clearer —ooo returns a function with ttt as the seed, of which the value of ttt is calculated by iii. Since x was passed into iii, we need to see where the values in the array were being set. At this point it was a trivial matter of going through each if-conditional, manually setting the elements of x to their correct values, and then calling the functions manually via the console:

x = [57, 88, 54]

Overall, this was one that unfortunately involved a fair bit of trial-and-error and tinkering due to my relatively poor knowledge of JavaScript. Something to consider in future would be to attempt replicating the browser’s environment such that the script’s various checks will set the values in the x array properly, and “repairing” the invitation so that it would work as expected by presenting the flag in a browser alert instead.

--

--