返回所有文章
發佈於 · 作者 Renaud Deraison

它請求被執行

Sonatype 正在追蹤 846 個惡意 npm 套件,全部從數百個拋棄式帳號、以 AI 生成的名稱發布。沒有任何一個帶安裝腳本。README 叫你用 require() 載入套件,而那個 import 就會執行載入器。靠名稱、作者或宣告的 hook 來辨識壞套件的防禦,如今是在跟一台生成器賽跑。Bromure Agentic Coding 攔的是生成器唯一偽造不了的性質:套件有多舊。

這些套件不帶任何安裝 hook,所以 npm 沒有東西可攔。README 叫你用 require() 載入它們,而那個 import 就會執行載入器。而照著這道指示走的 讀者,愈來愈常是一個程式編寫代理。

Sonatype Research Labs 在 8 月 5 日揭露了一場它稱為 Flooding Dropper 的行動,已清點出 846 個惡意 npm 元件,數字還在往上爬。 OpenSourceMalware 的 Paul McCarty 以 WEL1DROPPER 之名追蹤這個酬載。更廣的報導在 8 月 7 日 出爐。

有兩個細節,把這件事和過去一個月的 npm 供應鏈事件區隔開來。

第一是這些套件怎麼取得名字。攻擊者是把它們生成出來的,把 slopsquatting 當成一條生產線在跑。像 bigopsbnpl 這類反覆出現的字根,和其他字詞 重新組合成 bigops-apidolyame-boxy-desktop-bnpl-card-gallery 之類的 名稱,其中不少共用 35.x.y 的版本樣式。發布分散在數百個拋棄式帳號上, 每個帳號只掛幾個套件,所以根本沒有一個高產作者可以封。

沒有安裝腳本

這些套件完全不宣告生命週期 hook,所以 npm install 什麼都不會執行。 McCarty 的文章講得很直白:「這些 NPM 套件不使用 preinstallpostinstall 腳本。它不需要。」

它們附上一個打包好的 _helpers.js,在模組被 import 時執行,旁邊還有一份 README 告訴讀者去 import 它。用 require() 載入這個套件,文件這麼說,而 文件說得沒錯。第一階段會採集作業系統與處理器架構的指紋,透過 HTTPS 從三個 Cloudflare Workers 主機之一取回原生的第二階段;當直接取回失敗時,就退回到 從 wel1.ru 底下 DNS TXT 紀錄裡的 base64 片段重組出二進位檔。

第二階段依平台而異。Linux 拿到的是一個以 UPX 打包的 ELF,會布下 Sliver 的 C2 植入物。macOS 拿到的是反除錯檢查、對 VMware 的探測、一個 beacon 二進位檔,以及靠 LaunchAgent 的常駐。Windows 拿到的是 ETW 與 AMSI 的 修補、沙箱偵測、記憶體內的反射式執行,以及透過登錄檔 Run 機碼與排程工作的 常駐。McCarty 與 Sonatype 都把這場行動讀作四月那波相依混淆「Moika」的 演化版——當時推出了兩百五十來個以竊取環境變數為核心的套件。

攻擊者把扳機從套件管理器身上,移到了讀者身上。

三種讓人認不出來的方式名稱是生成的bigops-api, dolyame-boxy-desktop-bnpl-card-gallery,另約 844 個封鎖清單得先有名稱生成器還有更多名稱發布者是拋棄式的數百個帳號,每個只掛幾個套件封作者只封掉八百個裡的四個套件沒有安裝腳本沒有 preinstall、postinstallREADME 說:拿去 require()_helpers.js 在 import 時執行扳機就是讀者import 執行載入器採集 OS 與 arch,取回原生階段Cloudflare Workers,或 wel1.ru 底下的 DNS TXT
這場行動就是為了讓辨識失效而打造的。攻擊者是生成名稱而不是挑選名稱,讓發布者在數百個拋棄式帳號之間輪替,並把執行的扳機從生命週期 hook 移到 README 的一句指示上,於是掃描器找不到安裝腳本,封鎖清單也抓不到穩定的名稱或作者。

自動化打贏了封鎖清單

Sonatype 自己的總結,是整批報導裡最鋒利的一句:

把這句話讀成一段關於防禦的宣告。每一層靠著辨識出壞套件來保護你的防線, 無論是靠名稱、靠發布帳號,還是靠宣告的 hook,如今都在跟一台生成器競爭。 那些層仍然攔得住它們已經看過的東西。可是它們被評分的方式,是能不能認出 一串特定的字串,而特定字串的供應成本,不過是跑一次腳本。

代理這個角度讓事情更尖銳。slopsquatting 之所以一開始就划算,是因為語言 模型會發明聽起來很對、實際上不存在的套件名稱,而註冊了這些名稱的攻擊者, 等於免費收下安裝流量。再把缺席的安裝 hook 補上,你就得到一個以 README 裡一句書面指示為扳機的套件。程式編寫代理正是那個會幻想出名稱、在那個名稱 底下找到一個套件、讀完文件、然後照文件去做的讀者。這場行動的兩端,都是照 著代理的形狀塑出來的。

挑一個生成器偽造不了的性質。

年紀不是名稱

這場行動裡的每一個套件都共享一個性質,而且再多自動化也改不了:它是新的。 模型上週二才發明出來的名稱,底下不可能掛著一個 2019 年就存在的套件。數百 個拋棄式帳號發布數百個全新的版本,因為拋棄式帳號根本沒有可供發布的歷史。 這門生意仰賴的是:有人伸手去拿的時候,套件已經在那裡。所以攻擊者頂多提前 幾天把它建起來。

Bromure Agentic Coding 讓代理跑在 Apple Silicon 上一台拋棄式的 Linux VM 裡,並把它網路流量的每一個位元組,都導過主機端的代理伺服器, 在代理所處的那個盒子之外。npm、PyPI、Cargo、 RubyGems、Maven、NuGet、Go modules 與 Packagist 的套件抓取,全都會穿過那台 proxy,而它們遇上的第一條政策,是一座時鐘。

年齡閘門,預設開啟

Bromure 會拒絕比最低年齡還年輕的套件,出廠設定是兩天。像 latest 或 semver 範圍這樣的浮動參照,會解析到門檻之前最新的那個版本,所以一般的 安裝照樣能動,只是落在昨天的釋出上。釘死到太新版本的參照會收到 451, 附上一則清楚指名發生了什麼事的 Bromure 錯誤。擋下八百個套件的成本,和 擋下八個一樣,因為 Bromure 一個都沒有去辨識。

它在爭論開始之前就生效

這場行動聰明的地方,也就是丟掉生命週期 hook、把執行搬到 require() 上,講的是 tarball 落到磁碟之後的事。年齡閘門坐在抓取這一步,坐在主機 proxy 裡,在那一切的上游。酬載究竟會在安裝時執行還是在 import 時執行, 是一個關於「從未抵達的套件」的問題。VM 裡的 .npmrcpip.conf 可以把這條政策收緊,不能把它放鬆。

信譽,排在時鐘後面

有了 socket.dev 金鑰,Bromure 會在放行抓取之前,把每個套件送去比對 socket.dev 的資料庫,擋下被標記為遭入侵、惡意軟體或誤植搶註的那些。OSV 查詢會擋下帶有已知漏洞、且嚴重度達到你所選門檻以上的套件。Delpi 則乾脆 把每一個 npm 請求改導到一個具過濾能力的登錄庫。這些層都在「辨識」東西, 這讓它們有用,也讓它們排在第二位。

README 就照它的本質計分

「用 require() 載入這個」是一句陌生人寫的指示,以代理會讀到的文字形式 抵達。Bromure 會用一個本機的提示注入模型,替代理消費的工具輸出計分, 包含檔案內容、網頁與抓回來的文件。它也會替代理當成權威看待的指示檔—— CLAUDE.mdAGENTS.md 及其各種變體——用專屬的分類器,外加一支 隱形 Unicode 酬載掃描器來計分。做這些事,沒有任何東西離開這台 Mac。

酬載採集了錯的那台機器

假設抓取還是通過了,因為套件已經老過門檻,或有一條允許清單把它排除在外。 跟著 WEL1DROPPER 走過一個 Bromure profile,看它對著一台你本來就打算丟掉的 機器,把工作完整做完。

載入器的第一個動作是採集主機指紋。在 Bromure profile 裡,它採集到的主機是 一台 NAT 之後的 Ubuntu VM,離 macOS 隔著一層 hypervisor,於是它走 Linux 分支,在那裡種下一枚 Sliver 植入物。它從來不會去要 macOS 分支:那些反除錯、 那個 VMware 探測、那個 beacon_mac.bin 下載、那個原本能撐過重開機的 LaunchAgent、那份對俄羅斯支付網域的興趣。全部都是寫給一台這個 dropper 從沒見過的機器的程式碼。Erase home 會丟掉它落腳的那一層,把家目錄 重設回剛 clone 完的狀態。

接著 infostealer 階段去找它的祖先 Moika 去找的東西:環境變數。在 Bromure profile 裡,環境變數裝的是 brm_… 佔位符。真正的 Anthropic、OpenAI 和 xAI 金鑰,GitHub 與 GitLab 的 token,AWS 的憑證素材與登錄庫帳密,一個都不在 VM 的記憶體裡,也不在它的 dotfiles 裡。Bromure 把它們留在主機上,在 proxy 處、在線路上,只針對每一項所屬的那一個目的地主機,把它們代入請求。竊取 會一路跑完,寄出去的字串只在某一台 Mac 上有意義,而那台不是執行這段程式碼 的機器。

在開發者的 Mac 上它採集主機指紋macOS, arm64:走 mac 分支beacon_mac.bin、LaunchAgent、撐過重開機它讀取環境變數API 金鑰、git 憑證、雲端憑證素材全都是真的,全都能用而且它留下來它拿下的那台機器,就是你在用的那台在一個 Bromure profile 裡抓取先撞上一座時鐘今天發布,門檻兩天:451不必比對名稱,也不必特徵碼就算它真的抵達了VM 裡的 Linux,mac 分支用不到環境變數裡只有 brm_ 佔位符而且它留不下來Erase home 會重設它落腳的那一層
dropper 依它找到的主機挑選酬載。在 Bromure profile 裡,那台主機是一台拋棄式的 Linux VM,所以它從來不會去要那個帶 LaunchAgent 常駐、VMware 探測與 beacon 二進位檔的 macOS 分支,而 infostealer 階段讀到的是佔位符憑證,其真實值從未進入這台 VM。

他們建議的那場獵捕,早就在跑

McCarty 在 WEL1DROPPER 那篇文章結尾給了防禦建議:獵捕對 wel1.ru 的 DNS TXT 查詢、翻閱 proxy 日誌找往 Cloudflare Workers 主機的流量、留意散落的暫存 檔與 LaunchAgent。

把第二項再讀一次。你的筆電不會留 proxy 日誌。一個 Bromure profile 天生就 留著:VM 發出的每一個請求都穿過主機 proxy,而 Security Log 與 Trace Inspector 會記下每一筆的主機、狀態、延遲與代換報告。建置期間冒出三個陌生的 *.workers.dev 目的地,那就是清單上你讀得到的三列,就在跑這次建置的那台 機器上,不必為了拿到它們而部署任何東西。

登錄庫會繼續把這些套件下架,而下架是有效的:846 個已經消失或正在消失。 但這場行動早就過了一千個,發布用的帳號建起來不用錢,用的名稱發明出來也 不用錢,而下一波距離不過就是跑一次腳本。辨識,本來就是一場不該報名的比賽。

一個兩天大的套件,並沒有比兩小時大的多贏得什麼深層信任。它只是落在這門 生意運作的那扇窗子之外——而你可以用一個早就開著的設定,把你的相依圖整個 搬到那扇窗子後面。

安裝 Bromure Agentic Coding,讓抓取在遇上你的機器之前, 先遇上一座時鐘。