返回所有文章
发布于 · 作者 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 请求改导到一个带过滤能力的 registry。这些层都在“识别”东西, 这让它们有用,也让它们排在第二位。

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 的凭据材料与 registry 账密,一个 都不在 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 目的地,那就是清单上你读得到的三行,就在跑这次构建的那台 机器上,不必为了拿到它们而部署任何东西。

registry 会继续把这些包下架,而下架是有效的:846 个已经消失或正在消失。 但这场行动早就过了一千个,发布用的账号建起来不要钱,用的名字发明出来也 不要钱,而下一波距离不过就是跑一次脚本。识别,本来就是一场不该报名的比赛。

一个两天大的包,并没有比两小时大的多赢得什么深层信任。它只是落在这门生意 运转的那扇窗子之外——而你可以用一个早就开着的设置,把你的依赖图整个搬到 那扇窗子后面。

安装 Bromure Agentic Coding,让抓取在遇上你的机器之前, 先遇上一座时钟。