This was the preamble to Beware, a medium challenge categorized as reverse during the 24H@CTF :

Our malware detection system has spotted this script. Can you analyze it to see how it behaves? :) Disclaimer : The script is harmless but you must use a virtual machine if you want to run it or analyze it dynamically.

450 points, 11 solves

Reconnaissance

We are given beware.js. Let’s take a look at it :

Screenshot of the first few lines of the beware.js

From the first glance, I noticed a few things :

  • this looks like regular JavaScript ;
  • the names of variables and functions are obscured with Latin looking words ;
  • the strings are not plaintext ; and
  • there’s a lot of maths going on there.

Seeing the probably encrypted string and how the function is adding and subtracting characters together, I’ll a take a guess and assume that this is some sort of decryption function used to evade detection.

Scrolling down a bit, I saw something that is not standard JS:

Screenshot of WScript call

On line 33, I noticed WScript.CreateObject. This is a function that does not exist in regular JavaScript and because of some prior knowledge, I know that this must be JScript, a scripting language commonly used for malware stagers on Microsoft Windows.

I continued scrolling down and noticed something weird on line 83 :

Screenshot of single parameter call to nuptias()

There is a call to nuptias() (the decryption function) with only one parameter instead of two. That felt weird when I saw that, so I decided to take a second look at the function declaration

A closer look at nuptias()

Screenshot of nuptias()

The two parameters are called inpigre and artuum. The first one seems to be a string and the second seems to be an integer. But look closely at line 3 : artuum is reassigned, which means that the second parameter has no impact on the output of the function… If you calculate the value of artuum, you get that it is equal to 0.

The decryption start from the second-to-last character of the string because sunt, an iterator, is initialized to that index. If it’s the first iteration, the characters at inpigre[sunt] and inpigre[sunt+1] are parsed as a hexadecimal number, are interpreted as a character and appended to the end of manibus. If it’s not the first iteration, the same thing happen but the value of the previous character is subtracted from it and artuum is incremented by one. In the end, manibus is reversed and concatenated to form a string that is returned.

Now that I know that the second parameter is useless, let’s see if that means I can ignore all the maths.

Is math useless ?

$ grep -Po '^var \K\w+(?= = 0x.*)' beware.js

rabiem
respectu
ferebatur

That command returns the name of the first three variables, those that are used to do the maths. I want to know if these variables are something other than a second parameter for the decryptor.

$ grep -Ev '^(|var )(rabiem|respectu|ferebatur)' beware.js|grep -E 'rabiem|respectu|ferebatur'

var dextris = WScript.CreateObject(nuptias(cavatis[3], rabiem));
var rogati = WScript.CreateObject(nuptias(cavatis[5], respectu));
var pergunt = WScript.CreateObject(nuptias(cavatis[4], ferebatur));
dextris.RegWrite(nuptias(cavatis[0]), nuptias(cavatis[7], respectu));
adversando = dextris.expandEnvironmentStrings(nuptias(cavatis[1], rabiem));
adversando += nuptias(cavatis[2], rabiem);
rogati.WriteText(nuptias(cavatis[6], respectu));

That command will grep every line that does not start with an assignment of either of these three variables, but will grab every line that uses it. This allows me to know how those variables are used outside of their ridiculous amount of assignments. From the result of that command, I know that they are only used as a second argument to nuptias(), so I can safely just ignore those.

Summary

Let’s synthesize the information I have so far :

  • I have a file that looks like it might be a stager for some malware ;
  • almost all the objects’ names are replaced with Latin looking words ;
  • there is a lot of useless maths ; and
  • there is a function that decrypt a string.

With that in mind, let’s write a deobfuscator!

Deobfuscation

beware.js

After some coding, I have completed my deobfuscator. You can look at the code, I’ve written a lot of comments to guide you through it.

As for the deobfuscated code, here’s what I got :

var dextris = WScript.CreateObject('wscript.shell');
var rogati = WScript.CreateObject('ADODB.Stream');
var pergunt = WScript.CreateObject('Scripting.FileSystemObject');
dextris.RegWrite('HKCU\\Software\\G00gl3\\key', 'I4m_the_de0bfusc4t10n_key');
adversando = dextris.expandEnvironmentStrings('%APPDATA%');
dextris.CurrentDirectory = adversando;
adversando += '\\encoded_file.jse';
rogati.Open();
rogati.Type = 2;
rogati.Position = 0;
rogati.WriteText('try {a();} catch (aa) {eval(function(p,a,c,k,e,r){e=function(c){return(c<a?\'\':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!\'\'.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return\'\\\\w+\'};c=1};while(c--)if(k[c])p=p.replace(new RegExp(\'\\\\b\'+e(c)+\'\\\\b\',\'g\'),k[c]);return p}(\'u 8(a){7 b=[];7 j=0;h(7 i=a.e-2;i>=0;i-=2){F(i===a.e-2){b.k(l.m(v(a.f(i)+a.f(i+1),16)))}G{b.k(l.m(v(a.f(i)+a.f(i+1),16)-b[j].w(0)));j++}}x b.H().y(\\\'\\\')};u z(a,b){7 c=0;n=[];h(7 i=0;i<b.e;i++){c+=b.w(i)};h(7 j=0;j<a.e;j++){n.k(l.m(a[j]^c))}x n.y(\\\'\\\')};7 A=[I,d,o,B,C,D,J,d,K,p,L,q,d,M,q,q,N,d,p,d,O,p,D,r,o,r,B,C,P,d,Q,o,R,r,S,T];7 9="U.V.W.X.Y.Z.10".11(\\\'.\\\');7 s=t.E(8(9[1]));7 12=8(9[2])+z(A,s.13(8(9[0])))+8(9[5]);s.14(8(9[6]));7 g=15 17(8(9[1]));g.18(8(9[3]));g=t.E(8(9[4]));g.19(t.1a);\',62,73,\'|||||||var|urbes|indicium||||2379|length|charAt|laetabatur|for|||push|String|fromCharCode|rem|2423|2336|2343|2341|autem|WScript|function|parseInt|charCodeAt|return|join|nominibus|perfusorumque|2340|2401|2424|CreateObject|if|else|reverse|2397|2416|2428|2402|2422|2426|2393|2407|2375|2406|2404|2400|938e98b1afc2d5daebd8d3d7c1a3776097d39f8fc7d0de79|ead6d5dbd9e4a2a1dbcdd1d86c|928d8874a87b|a7dcd8d3d7858fd5868dd28621|b6d5dbd9e4ddd7d59574afd5d1b8ccece7d9d2bcb1cccfc8d774|7d|938e98b1afc2d5daebd8d3d7c1a3776097d39f8f5c|split|vexillum|RegRead|RegDelete|new||ActiveXObject|Popup|DeleteFile|ScriptFullName\'.split(\'|\'),0,{}))};');
rogati.SaveToFile(adversando, 2);
rogati.Close();
dextris.Run('"' + adversando + '"', 0, false);
pergunt.DeleteFile(WScript.ScriptFullName);

The script begins by creating objects to run commands (wscript.shell) and interact with the file system (ADODB.Stream and Scripting.FileSystemObject). It then stores some some of decryption key to the registry and open %APPDATA%\encoded_file.jse to write some packed JavaScript. Once that is done, it runs the newly written file and delete itself. At first, we supposed that this file was malicious, but now we know that it certainly is.

%APPDATA%\encoded_file.jse

I copied the rogati.WriteText string and unpacked it using a little python snippet. This snippet needs jsbeautifier installed. That can be done by running pip install jsbeautifier. When running, you just paste the string and it will write the unpacked version to `decoded_file.js :

python -c "from jsbeautifier import Beautifier;p=input();print(Beautifier().beautify(eval(p)))" > decoded_file.js

Screenshot of decoded_file.js

When I opened decoded_file.js, I immediately recognized urbes() as being the decryptor that we saw earlier. When I looked at nominibus(), I needed a bit more time.

nominibus() seems like it’s initializing c by adding together all the characters from b together. Then, there is the decryption phase where it XORs each character from a individually with c and append them to rem. It returns the resulting array as a string. For nominibus(), both parameters seems useful.

Let’s write some deobfuscator for this new script based on what I had made earlier. After running the code through it, I get that output :

var perfusorumque = [2397, 2379, 2423, 2340, 2401, 2424, 2416, 2379, 2428, 2336, 2402, 2343, 2379, 2422, 2343, 2343, 2426, 2379, 2336, 2379, 2393, 2336, 2424, 2341, 2423, 2341, 2340, 2401, 2407, 2379, 2375, 2423, 2406, 2341, 2404, 2400];
var autem = WScript.CreateObject('wscript.shell');
var vexillum = 'FLAG-{' + nominibus(perfusorumque, autem.RegRead('HKCU\\Software\\G00gl3\\key')) + '}';
autem.RegDelete('HKCU\\Software\\G00gl3\\');
var laetabatur = new ActiveXObject('wscript.shell');
laetabatur.Popup('Beware of me!');
laetabatur = WScript.CreateObject('Scripting.FileSystemObject');
laetabatur.DeleteFile(WScript.ScriptFullName);

The script creates a wscript.shell object, decrypt and print the flag, delete the registry key that contained the decryption key, launch a popup that says Beware of me! and finally delete itself. In an out-of-CTF context, I could say that this script is inoffensive and move on, but there is a line with FLAG-{ and a call to what I’ve called xor_decrypt in my deobfuscation script.

To get the rest of the flag, I added a feature to my script that makes it possible to call that function when the script is run without arguments. Using the value stored in the register in the last script, I ran :

$ ./beware-deobfuscator2.py

xor_decrypt(arg1, arg2)
  arg1=[2397, 2379, 2423, 2340, 2401, 2424, 2416, 2379, 2428, 2336, 2402, 2343, 2379, 2422, 2343, 2343, 2426, 2379, 2336, 2379, 2393, 2336, 2424, 2341, 2423, 2341, 2340, 2401, 2407, 2379, 2375, 2423, 2406, 2341, 2404, 2400]
  arg2=I4m_the_de0bfusc4t10n_key
I_c0uld_h4v3_b33n_4_M4l1c10us_Scr1pt

There we have it, the encrypted part of the flag. Putting all the parts together, we get : FLAG-{I_c0uld_h4v3_b33n_4_M4l1c10us_Scr1pt}

Lessons learned

I think that this challenge was entertaining. This was the first time I’ve written a deobfuscator, but it was definitely not necessary. A simple Search and Replace in your favorite editor and a Node.js interpreter would be enough to quickly find the flag, but I think there is more to learn by looking at it as if it was more than just a challenge. Like that, the next time I encounter obfuscated code that can’t be solved with search-and-replacing, I’ll be a step ahead.

Thanks to @Edouard#6067 from PolyHx for this challenge :-)