npm「btree」惡意套件攻擊:不靠安裝腳本,惡意程式碼藏在套件主程式裡
資安研究
2026-09-17 by 高田鑑識
本文整理自 Checkmarx Zero 研究團隊發表的〈npm ‘btree’ Malware Campaign Affects Millions of Downloads, No Need for Install Script〉。原文為持續更新的研究文章,請以原文最新內容為準。
Checkmarx Zero 揭露一起仍在進行中的 npm 供應鏈攻擊:攻擊者利用智慧合約作為命令與控制(C2)管道,並把惡意程式碼藏在套件本身的主程式裡,而非安裝腳本中。截至原文發表時,攻擊者據估已獲利 109 ETH,約 23 萬歐元。
重點摘要
- 惡意套件:indexed-btree,偽裝成正常的 B 樹/索引工具 sorted-btree,週下載量接近 200 萬。
- 不使用安裝腳本:package.json 中沒有 preinstall/postinstall,只安裝不會有任何動作;惡意程式碼在應用程式執行時才觸發。
- C2 藏在區塊鏈上:惡意程式讀取 Sepolia 測試網上的智慧合約取得指令與位址,比傳統網域或 IP 更難封鎖。
- 相關套件:另有 9 個後續被下架的套件使用同一套手法,其中 btree-core 累計下載近 195 萬次。
為什麼這件事值得注意:安裝腳本封鎖之後的攻擊轉移
npm 在 v12 版本限制了生命週期腳本(lifecycle scripts)的執行,不少人因此認為「沒有安裝腳本的套件比較安全」。Checkmarx 先前就預測攻擊者會調整手法,這次的案例正好印證:惡意行為只是從「安裝階段」延後到「載入並使用套件的階段」。
這也讓防守方容易產生誤判——只因為套件沒有安裝腳本,就認定它是正常的。事實上,indexed-btree 在這樣的情況下,仍然達到了每週近 200 萬次下載。
觸發方式:藏在最常被呼叫的方法裡
惡意載入程式被放在套件自己的 BTree.prototype.set 方法裡,也就是使用者幾乎每次都會呼叫的主要寫入函式。當條件成立時(原文範例中是 key 等於 100),程式會用 child_process.spawn 在背景啟動 extended/sharedLoad.min.js,以分離的子程序執行,不輸出任何訊息、也不顯示視窗,整段包在空的 try/catch 中,失敗時完全不會出現錯誤。
這是一種很有效的藏匿方式:安裝階段完全乾淨,感染點位在覆寫的原型方法裡,只要套件被實際使用就幾乎必然觸發,因此能繞過許多只看安裝腳本的檢測方式與一般靜態掃描工具。
以下是這段載入邏輯的簡化示意(非原文逐字內容):
BTree.prototype.set = function (key, value, overwrite) ;
惡意程式的行為
套件隨附的檔案經過高度混淆(例如字串陣列編碼與具自我校驗的陣列旋轉),主要功能如下:
- 蒐集並外傳主機資訊:作業系統、架構、主機名稱,以及 CPU、記憶體與開機時間等資訊,會傳送到寫死的 Slack 頻道與 Telegram 聊天室。
- 以智慧合約作為 C2:惡意程式輪詢部署在 Sepolia 測試網上的合約,透過合約的讀取與寫入函式取得資訊,而不是連線到固定網域。合約等於一個「指標」,舊位址被查封後,只要更新合約內容就能指向新位址,比傳統 C2 更耐得住網域或 IP 的查封。
- 以鏈上金鑰交換解鎖第二階段:載入程式自行產生 X25519 金鑰對,並從合約取得攻擊者的 X25519 公鑰,透過 ECDH 計算共享密鑰,再導出 AES 金鑰,用來解密儲存在合約中的兩段密文;兩段合併後就是第二階段惡意程式。
- 清除痕跡:具備刪除惡意檔案,以及從主程式中移除觸發程式碼的功能,增加事後調查的難度。
為什麼能被大量下載:精心包裝的「正常」外觀
這起攻擊在偽裝上相當用心:不僅沒有明顯的安裝腳本,還建立了一個外觀正常的 GitHub 儲存庫,裡面有大量看似合理的提交紀錄,而且儲存庫本身刻意不放惡意程式碼。攻擊者通常不會花力氣做這件事。
發佈者的 GitHub 帳號則幾乎沒有活動,也沒有詳細簡介,但使用了 AI 生成的大頭照,乍看之下更像真人。這些細節都可能讓開發者在挑選套件時放下戒心。
入侵指標(IoC)
受影響套件
| 套件 | 累計下載 |
|---|---|
indexed-btree(惡意樣本,偽裝成 sorted-btree) |
週下載量近 200 萬 |
btree-core |
1,951,274 |
btree-leaderboard |
493,685 |
btree-range-store |
468,092 |
ordered-kv-index |
448,184 |
sliding-score-window |
448,024 |
btree-time-index |
425,312 |
priority-slot-queue |
402,860 |
btree-lru-cache |
372,185 |
neighbor-key-map |
366,019 |
原文指出,攻擊者使用的智慧合約過去也曾出現在其他套件中,例如 mutex-forge。
智慧合約(以太坊 Sepolia 測試網)
- 合約位址:
0xE390863Dac96a7118C71227C2b099B50cF602D31 - 攻擊者寫死的 X25519 公鑰(SPKI/DER,十六進位):
bad013df6eec5d686f4cc8551e0a5c87a0135164bdd1dafb1c75141d1b526702
外傳管道
- Telegram:聊天室 ID
-1003952553968 - Slack:頻道 ID
C0B8XPGCKQS
原文另列有攻擊者使用的機器人權杖與區塊鏈 RPC 服務的 API 金鑰,這些屬於憑證類資訊,本文不重複刊載,需要時請參閱原文。
如何檢查與處置
1. 檢查是否使用相關套件(含間接相依)
npm ls indexed-btree ordered-kv-index btree-leaderboard priority-slot-queue btree-range-store btree-core btree-time-index btree-lru-cache neighbor-key-map sliding-score-window --all
2. 搜尋鎖定檔:被下架的套件仍可能留在 package-lock.json、pnpm-lock.yaml 或 yarn.lock 中,也可能還存在私有套件庫與快取裡。
3. 檢查套件目錄:檢視 node_modules 中是否存在 extended/sharedLoad.min.js,並留意 BTree.prototype.set 內是否有 child_process 的呼叫。
4. 若確認使用過:視為主機與該環境中的憑證都可能外洩,包含環境變數、CI/CD 金鑰、雲端帳密與 API 權杖,請輪替相關憑證,並檢視對外連線紀錄中是否出現 Telegram、Slack 或區塊鏈 RPC 的異常流量。
5. 調整檢測重點:封鎖安裝腳本仍值得做,但不能只靠這一層。需要搭配執行期的行為分析,並在選用套件時檢查名稱是否仿冒、發佈者歷史與實際程式碼內容,而不是只看有沒有安裝腳本或儲存庫是否好看。
高田觀點
這起事件說明了一件事:防禦手段一旦成為標準做法,攻擊者就會往下一個沒被覆蓋到的階段移動。對事件調查而言,「安裝階段乾淨」不能作為排除嫌疑的依據;需要回頭檢視套件實際被載入後的行為、子程序的建立紀錄,以及對即時通訊服務與區塊鏈節點的對外連線,這些才是還原感染過程的關鍵證據。





