Tags: javascript web anti-debugging unicode 

Rating:

**Description**

> You stumbled upon someone's "JS Safe" on the web. It's a simple HTML file that can store secrets in the browser's localStorage. This means that you won't be able to extract any secret from it (the secrets are on the computer of the owner), but it looks like it was hand-crafted to work only with the password of the owner...

**Files provided**

- [a ZIP file](https://github.com/Aurel300/empirectf/blob/master/writeups/2018-06-23-Google-CTF-Quals/files/js-safe.zip) containing:
- `js_safe_2.html` - an HTML file with obfuscated JavaScript

**Solution**

We are presented with a website showing a password input. After entering a password, it seems to check whether or not it is correct and says `ACCESS DENIED`.

![](https://github.com/Aurel300/empirectf/raw/master/writeups/2018-06-23-Google-CTF-Quals/screens/js-safe1.png)

Let's see the source code. There is a comment:

There is some CSS to make the website look pretty, some HTML to create the form, but most importantly the JavaScript:

<script>
function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}return chr(b>>8)+chr(b&0xFF)+chr(a>>8)+chr(a&0xFF)}function c(a,b,c){for(i=0;i!=a.length;i++)c=(c||'')+chr(ord(str(a[i]))^ord(str(b[i%b.length])));return c}for(a=0;a!=1000;a++)debugger;x=h(str(x));source=/Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/;source.toString=function(){return c(source,x)};try{console.log('debug',source);with(source)return eval('eval(c(source,x))')}catch(e){}}
</script>
<script>
function open_safe() {
keyhole.disabled = true;
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !x(password[1])) return document.body.className = 'denied';
document.body.className = 'granted';
password = Array.from(password[1]).map(c => c.charCodeAt());
encrypted = JSON.parse(localStorage.content || '');
content.value = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}
function save() {
plaintext = Array.from(content.value).map(c => c.charCodeAt());
localStorage.content = JSON.stringify(plaintext.map((c,i) => c ^ password[i % password.length]));
}
</script>

`open_safe` is called when we enter a password. `save` seems to be a function used once the "safe" is unlocked. Apparently it just uses the `localStorage` (present in the browser and saved on the disk) and XOR encryption to "securely" store data.

Important to us is the `password` regular expression - we need to enter `CTF{...}` as the password, using only numbers, letters, and the symbols `_`, `@`, `!`, `?`, and `-` between the curly braces. The actual password check is done by calling the function `x` with the inner part of the password (excluding `CTF{` and `}`).

Let's prettify the function `x` and look a it bit by bit:

function x(х){
ord=Function.prototype.call.bind(''.charCodeAt);
chr=String.fromCharCode;
str=String;

First, three shortcut functions are defined. `ord`, `chr`, and `str`.

- `ord` - converts a (Unicode) character to its Unicode codepoint, a number
- `chr` - converts a Unicode codepoint to its string representation (opposite of `ord`)
- `str` - stringifies a value

function h(s){
for(i=0;i!=s.length;i++){
a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;
b=((typeof b=='undefined'?0:b)+a)%65521
}
return chr(b>>8)+chr(b&0xFF)+chr(a>>8)+chr(a&0xFF)
}

The next, inner, function is `h`, which seems to be a simple hashing algorithm. It iterates all the characters of its argument, adding their Unicode values to an accumulator, and adding the accumulator to another indirect accumulator. Finally, the two variables `a` and `b` are converted to a 4-byte string (which may or may not be 4 characters!).

function c(a,b,c){
for(i=0;i!=a.length;i++)
c=(c||'')+chr(ord(str(a[i]))^ord(str(b[i%b.length])));
return c
}

Finally, `c` is an encryption function. It simply XOR encrypts `a` using a repeating key `b`.

for(a=0;a!=1000;a++)
debugger;

Then we see the first *obvious* anti-RE technique. It is a loop that runs 1000 times and tries to run `debugger` each time. This only has effect if the browser's developer tools are open. We can remove this loop to prevent this. This lets us debug the function, but it is also a big mistake - we'll see why in a bit!

x=h(str(x));

Then `x` is assigned to the hash of the stringified value `x`. This should be the password, right?

Wrong - if we put a `debugger` statement just before this line, we can check what the value of `x` is. We can simply type `x` into the console, assuming we are currently paused inside the function. `x` always evaluates to the function itself, not the argument provided! It's not unusual that you can refer to the function within itself, this is what makes recursion possible. But what is weird is that the argument given to this function is also `x`, so thanks to aliasing inside the function `x` should refer to the argument, not the function.

Well, after some more analysis, we can find out that this script uses a fairly common technique in JavaScript obfuscation. The argument of the function is not `x`. It is `х`. Most fonts will render these characters the same, but the latter is actually [cyrillic small letter ha](https://unicodelookup.com/#%D1%85/1). We can confirm this if we look at the hexdump of the file. What is also curious is that the argument is never mentioned inside the function except in the function signature.

Anyway, after executing the last line, `x` is now the hash of the stringified version of the function itself. This is important - we have modified the function already by prettifying it. Any whitespace is kept by JavaScript when it stringifies the function. But we can circument this - we open the original file, find out the string representation of the original `x` function, and in our working copy we put:

x = h("function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}return chr(b>>8)+chr(b&0xFF)+chr(a>>8)+chr(a&0xFF)}function c(a,b,c){for(i=0;i!=a.length;i++)c=(c||'')+chr(ord(str(a[i]))^ord(str(b[i%b.length])));return c}for(a=0;a!=1000;a++)debugger;x=h(str(x));source=/Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/;source.toString=function(){return c(source,x)};try{console.log('debug',source);with(source)return eval('eval(c(source,x))')}catch(e){}}");

Moving on:

source=/Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/;
source.toString=function(){return c(source,x)};

`source` is given as a regular expression, containing a bunch of Unicode data. It is important to parse it as Unicode, since this is how JavaScript understands string values (and regular expressions as well). Additionally, `toString` is set on `source` to run the encryption function. This last bit is somewhat weird. JavaScript does use the `toString` method of an object (if it exists) when it tries to stringify it. However, inside `c`, the first argument needs to be stringified. I believe this is another, slightly more subtle, anti-RE technique - every time we try to see what the value of `source` is in the console, it results in an infinite loop.

try{
console.log('debug',source);
with(source)return eval('eval(c(source,x))')
}catch(e){}

Finally, in a `try ... catch` block, we first have a `console.log` statement, which will enter the aforementioned infinite loop if the developer tools are open. Then a rarely used feature of JavaScript, [`with`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) is used to make the properties of `source` available globally. In particular, this might mean that the global `toString` function is the one defined on `source`, which will always return `c(source, x)`. Inside the `with` block, we perform a nested `eval` - `c(source, x)` is executed as JavaScript code and the result of this is once again executed as JavaScript code.

Any errors that might occur in this process are caught and silently ignored because of the `try ... catch` block. For this entire password check to be successful, the `x` function needs to return a [truthy value](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), and the only `return` is the value obtained from the double-`eval` construct.

There is one final anti-RE technique that is more subtle than the others. Normally, JavaScript variables are declared with `var`. In more modern syntax, `let` or `const` is used. Not a single `var` can be found in the script. This is not an error (a mantra in JavaScript) - the code still runs just fine. Whenever a variable is used without declaring it with `var`, the global scope will be used. This generally means the variables are created on the `window` object. However, function argument names are implicitly declared as local variables and these will not be created as `window` properties! With this in mind, let's list all the global variables used in the script:

- `x` - initially the password-checking function, later overridden with the function's hash
- `ord`, `chr`, `str`
- inside the function `h`:
- the loop variable `i` is global, although it is zeroed out before use
- `a`, `b` are both global and a default value is only used if they were `undefined` (i.e. undeclared) beforehand
- in the anti-RE `debugger` loop, `a` is global
- `source`

We can notice something very important in the list above - the global variable `a` is used both inside `h` and inside the anti-RE loop. Before `h` is even called for the first time, `a` is already set to `1000`. If we simply remove the loop, `a` will be `undefined` before `h` is called, giving an incorrect result.

Now we can start peeling off the layers of protection. As mentioned above the actual password value (stored in the cyrillic `х`) has not been used in any of the code we can see so far. This means that the first steps of decrypting the `source` variable are not done based on user input, so we should be able to reproduce them ourselves.

Keeping in mind that `a` is initialised to `1000` before `h` is called, we can calculate the hash of the function:

let ord = Function.prototype.call.bind(''.charCodeAt);
let chr = String.fromCharCode;
let s = "function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}return chr(b>>8)+chr(b&0xFF)+chr(a>>8)+chr(a&0xFF)}function c(a,b,c){for(i=0;i!=a.length;i++)c=(c||'')+chr(ord(str(a[i]))^ord(str(b[i%b.length])));return c}for(a=0;a!=1000;a++)debugger;x=h(str(x));source=/Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/;source.toString=function(){return c(source,x)};try{console.log('debug',source);with(source)return eval('eval(c(source,x))')}catch(e){}}";
let a = 1000;
let b = 0;
for (let i=0; i != s.length; i++) {
a = (a + ord(s[i])) % 65521;
b = (b + a) % 65521;
}
let ret = chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF);
console.log([b >> 8, b & 0xFF, a >> 8, a & 0xFF]);
console.log(ret);
console.log(ret.length);

The first `console.log` prints `[130, 30, 10, 154]`. None of these are printable characters, but JavaScript still considers the string to be 4 characters long.

So let's try to XOR-decrypt `source` with the above key. Even though it is given as a regular expression with a weird `toString` function, let's just see what happens if we put it in a string and decrypt that:

let ord = Function.prototype.call.bind(''.charCodeAt);
let chr = String.fromCharCode;
let a = "Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨";
let c = "";
let points = [];
for(i = 0; i != a.length; i++) {
let point = ord(a[i]) ^ [130, 30, 10, 154][i % 4];
points.push(point);
c = c + chr(point);
}
console.log(points);
console.log(c);

Indeed, we get some valid JavaScript code!

х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&›¨þJ',h(х))//᧢

And in this fragment the `х` is actually the cyrillic `х` which contains our password. We are looking for a password which equals *something* when XOR decrypted with its own hash. We have two unknowns, both the password and its hash, so the solution is not direct.

However, we know enough about the hashes and the password to break the cipher. The hashes returned by `h` are only 4 bytes long. This can decode into less than 4 characters if these bytes represent valid Unicode codepoints, but let's just treat them as 4 separate 8-bit integers. A key that is only 4 bytes long repeated over a 39+ character-long password is very vulnerable, since a quarter of these characters are decrypted with the same key character.

And finally, we know that the password only consists of the characters `0-9a-zA-Z_@!?-`, which combined with the repeating XOR key should be more than enough.

([full solver script](https://github.com/Aurel300/empirectf/blob/master/writeups/2018-06-23-Google-CTF-Quals/scripts/JS.hx))

$ haxe --run JS.hx
_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_

Three of the hash numbers have a unique solution, one has two. One of these two gives the correct solution.

![](https://github.com/Aurel300/empirectf/raw/master/writeups/2018-06-23-Google-CTF-Quals/screens/js-safe2.png)

`CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}`

Original writeup (https://github.com/Aurel300/empirectf/blob/master/writeups/2018-06-23-Google-CTF-Quals/README.md#121-web--js-safe-20).