他们先把安全的版本撤了下来
8 月 20 日,有人把一个被投毒的 arrayref 推上 crates.io,然后在十六秒之内,把它身后那些安全的版本一一撤下。于是 Cargo 自己的警告,就指向了唯一还站着的现代版本。恶意载荷藏在构建脚本里,所以光是对项目做一次类型检查就足以把它跑起来。Bromure Agentic Coding 把整场行动变成一道它必输的秒表题:被投毒的 crate 只活了 86 分钟,而默认的年龄门是两天。
攻击者根本不需要骗谁去装恶意软件。他让干净的版本消失,剩下的就交给注册表自己的升级 建议去完成。
8 月 20 日 07:15:00 UTC,[email protected] 以它真正维护者的账号出现在 crates.io 上。
二十四秒之后,攻击者开始 yank 掉 0.3.5 到 0.3.9:十六秒的脚本工作,把它身后的货架清空。
yank 是 crates.io 的召回机制,是维护者发现某个发布坏掉时会拉的那根杆子。Cargo 对它的 回应正是你希望的样子。它会警告你锁文件里有个版本已被撤下,并且在重新解析时拒绝选中被 撤下的版本。所有跟在这类警告后面的建议,都叫你去更新。
于是攻击者撤掉安全的版本,只留一个现代版本站着,让“做该做的事”把你送进去。
九十分钟,从头到尾
Rust Security Response WG 的公告 和 StepSecurity 重建的时间线 描述了一场攻击者已经铺垫两天的行动。
8 月 18 日 01:25:58,他们在 crates.io 注册了一个叫 dtolney 的账号,跟 David Tolnay 只
差一个字母;生态里大多数 proc-macro crate 上都挂着那个名字。半小时后,他们发布了一个
干净的 [email protected],那是 proc-macro2 的仿冒名,什么都不做。那个发布存在的
唯一目的,是给这个账号一段历史。
真正的那个五小时后才来。[email protected] 在 8 月 20 日 07:11:15 上线,带着一个恶意
的 build.rs。四分钟之后,[email protected] 从合法所有者的账号发出,里面只有一处改动:
一行新的依赖,指向 proc-macro1 ^1.0.107。Security Response WG 在维护者这件事上很谨慎,
而且谨慎得对。他们“不认为 arrayref 的作者是在恶意行事”,并指出“他的电脑或凭据很可能
已被入侵”。[email protected] 在 07:34:07 跟上,[email protected] 在 07:37:49。
然后它结束得几乎跟开始一样快。Nextron Systems 的研究团队找到了那个恶意 crate 并上报。
crates.io 在 86 分钟后撤下 arrayref,90 分钟后撤下 internment,107 分钟后撤下
append-only-vec,删掉六个攻击者持有的 crate,并锁住那个被入侵的账号。RustSec 没有记录
到任何人对着被投毒的版本构建过的证据。
九十分钟是很快的响应。但对任何在那个窗口里跑过构建的人来说,它依然是事后才到。
库里没有任何东西需要被调用
恶意载荷住在构建脚本里,而构建脚本会在编译期以你的权限运行。The Hacker News 报道称载荷会
在 cargo build、cargo check 和 cargo test
时触发。中间那个是谨慎的选项:你的编辑器保存时会跑的那个,你想听听编译器意见但什么都不想
执行时会敲的那个。在这里,它就够了。
脚本本身一旦摊在你面前,其实又短又不含蓄。它从 base64 片段重组出
https://23.254.165[.]112:9089/,关掉 TLS 证书校验,下载一份匹配你操作系统与架构的载荷,
然后分离运行。漂亮的一手是 std::mem::forget(child),丢掉那个句柄,好让进程逃出 Cargo 的
job object。植入体继续跑,而编译器以一次绿色的构建收尾。
接着下来的是一个信息窃取器。
Wiz 的分析
说它会用 HTTPS POST 向 /49890878 回连,收集主机名、用户名、操作系统与已安装应用,并查询
Chrome、Brave 与 Edge 的 SQLite 登录数据库里保存的凭据。它在 Windows 上以注册表 Run 键
持久化,在 macOS 上用 LaunchAgent,在 Linux 上用 systemd 用户服务;如果 C2 失联,就退回一
套域名生成算法,每五天生成十个 .com 域名。Wiz 把这批基础设施跟朝鲜的活动联系起来:与
Mastra npm 行动相同的 /49890878 端点、共用的证书签发者,以及反复使用的同一段 Hostwinds
地址。
它发出的构建覆盖 x86_64 的 Linux、Windows 和 macOS,外加 aarch64 的 macOS。最后那个是一台 Apple Silicon 笔记本。有人想过谁在编译 Rust。
你从没选过的那个依赖
arrayref 累计有 2.45 亿次下载,而几乎没有人把它写进 Cargo.toml。StepSecurity 数出 406
个依赖它的 crate 版本,arrayref 坐在 winit 底下,因而也在 egui 和 iced 底下,在
blake3 和 blake2b_simd 底下,还在相当一部分 Ethereum 与 Solana 工具链底下。internment
和 append-only-vec 两者又加上 1900 万次下载。
所以“加依赖前先审一遍”这条建议在这里够不着。没有人加过它。它出现在四层之下,在一个你确实 加过的 crate 底下,在一份一滑而过的锁文件差异里。读库的源码同样帮不上忙。三个被投毒的 crate,没人动过它们的源码;整个改动就是一行依赖,点名一个两天大的 crate,而发布它的账号 名字看起来是对的。
一个谈智能体编码的博客为什么在乎
再看一遍那个触发点。构建输出里跳出一条 yank 警告。某个东西跑了 cargo update 把它清掉。
那个东西,如今常常已经不是人。清掉警告是智能体在奔向你交代的事情途中顺手做的家务:它看到 一个被撤下的版本,它知道怎么修,它就修。它在你睡觉时工作,在循环里工作,横跨整批待办。它 因为构建失败而刷新锁文件,因为某个功能需要而加进新的 crate。
一个无人看管在工作的智能体,是最可能落在那 86 分钟窗口里的角色,也是最不可能为了一个由它 认得的名字发布、自己却没见过的传递依赖而停下来的角色。给它一个被入侵的维护者账号、一个跟 David Tolnay 只差一个字母的仿冒名,再加上一条建议升级的注册表警告,这条链上就没有任何一环 留给判断去拦。
你还是活得下来,因为判断本来就是决定这件事的糟糕地方。
Bromure 把它变成一道秒表题
每一份 Bromure Agentic Coding 配置文件出厂时都开着年龄门,设在两天。主机端的 MITM 代理 看得到 VM 发出的每一次包获取,横跨 npm、PyPI、Cargo、RubyGems、Maven、NuGet、Go 模块 和 Packagist,而比阈值更年轻的版本回不来。
Cargo 拿到的是完整待遇。Bromure 认得 index.crates.io、crates.io 和 static.crates.io,
然后在途中改写 crate 的元数据:把太新的版本从版本列表里剔掉,并把 max_version、
max_stable_version 和 newest_version 设成通过过滤后最新的那个发布。Cargo 的解析器从来
不会知道有更新的版本存在。
现在把数字并排放。
这场行动里没有任何东西活到两天大。arrayref 把 [email protected] 点名为依赖时,它才
四分钟大。对门后面的解析器来说,这两个 crate 都不可见,所以 cargo update 找不到任何新
东西。
你会在屏幕上看到这件事。0.3.5 到 0.3.9 被 yank、0.3.10 被过滤掉之后,对 arrayref 做一次
全新解析,可能会回来说选不出版本——那是一条错误,而不是一次无声的成功。已经钉在 0.3.9
的现有 Cargo.lock 会继续构建,因为 yank 只挡新的解析。无论哪种,你那个周四早上都花在读
一条解析器消息,而不是轮换你手上所有的凭据。
Bromure 还会再查第二次,为的是解析器早就有答案的情况。它拿每一次制品获取,也就是 .crate
文件本身,去比对记录下来的发布时间,并返回一个把年龄说明白的 451:发布于 41 分钟前,
策略要求至少 2 天。一份热的元数据缓存,买不到攻击者任何东西。
投放器的第一跳哪儿也去不了
假设你把钟关掉了。有人给某个包开了例外,或者把阈值降低一个下午。构建脚本还是得连上
23.254.165.112 的 9089 端口。
每一份 Bromure 配置文件都带着自己的出站策略,写成一小段 pf 风格的规则集:
allow web crates.io
allow web static.crates.io
allow web index.crates.io
allow tcp api.anthropic.com:443
default deny
你的 Mac 在共用同一份规则的两层上执行它:虚拟交换机,跨所有协议按目的 IP 与从 DNS 里嗅到
的主机名匹配;以及代理,按 TLS SNI 与 HTTP 方法匹配。9089 端口上一个赤裸的 IP,在那个文件
里对不上任何规则,于是落到 default deny,连接在边界的主机这一侧就死了。第二阶段的 DGA,
每五天十个新的 .com 域名,撞上同一面墙,因为生成出来的名字,仍然是没人放进清单里的名字。
每一次拒绝都会以一行 egress.firewall 落在 Window → Security Timeline,带着主机、IP、
端口和判定,写在 VM 里的代码够不着的地方。
那里面没有东西可偷
读一读这起事件附带的恢复清单:轮换 SSH 密钥、云令牌、API 令牌、签名密钥。清掉注册表缓存。 清掉 CI 的缓存层。重建 vendor 目录。在每一台曾在那个窗口里构建过的机器上。
一个 Bromure 工作区用一间空屋子回答了这份清单的大半。
真正的凭据从来没进过 VM。那个客户机里的 ANTHROPIC_API_KEY 是一个 brm_… 的占位串。
kubeconfig 里放的是用完即弃的客户端证书。容器注册表的认证是一串派生出来的 Basic 字符串。
GitHub 令牌是假的,SSH 密钥是为那个工作区逐配置文件生成的 ed25519 密钥,不是你笔记本上那
把。主机端的 MITM 代理会在出站请求上换成真值,而 AWS 还更进一步:主机会用真正的密钥材料
重新签署 SigV4 请求,所以任何绕过代理的东西拿回来的是 Amazon 的
InvalidSignatureException,而不是一次能用的调用。
一个在那个客户机里扫凭据的信息窃取器,会找到凭据。它们解析得过去,看起来也对,而且一离开 那台机器就一文不值。
它购物清单上的其余部分,落在虚拟机监控器的另一边。Chrome、Brave 和 Edge 把登录数据库留在
你的 Mac 上,放在客户机没有路径可达的用户配置里。它想写的那个 macOS LaunchAgent,也就是它
为什么要出一份 aarch64 macOS 构建的理由,压根没有 macOS 可写。它可以在客户机自己的家目录
里装一个 systemd 用户服务,而那一层有一个按钮:Erase home… 会把配置文件的
/home/ubuntu 重置回刚克隆完的状态,Reset to base… 则从只读的基础镜像重新克隆工作区
的系统盘。
而且你早就有那份清单了
这起事件每一份修复指南的最后一行都是一次 grep:在每一台机器的每一份 Cargo.lock 里搜六个
crate 名字和三个版本字符串,然后去 ~/.cargo/registry/cache 里搜那些 tarball。那是很好的
指示,也是很惨的一个下午,而且它只在你还留着的机器上管用。
Bromure 在事情发生的当下就把答案记了下来。每一次穿过代理的包获取,都会在
Window → Security Timeline 写下一行 supply_chain.fetch,带着生态、包、版本、结果,
以及结果背后的理由,记在主机这一侧,跟每一次凭据替换和每一次防火墙判定并排。等下一条公告
点名某个版本和某个两小时窗口时,“这里有东西抓过那个吗?”就是一次搜索,而不是一场远征。
Bromure Enterprise Manager 把同一道数据流在整个机群上汇总起来,而问题通常正是以那种形式 抵达:昨晚跑智能体的那九十台机器里,有没有哪台构建了它。
值得留下的那部分
crates.io 这回做得好。从上报到移除九十分钟,六个攻击者的 crate 消失,被入侵的账号锁住,一 条清楚的公告,而且没有任何人中招的证据。Rust 团队还特意不去责怪一位凭据被偷的维护者,那是 对的直觉,而且不总是常见的直觉。
不过,总会有人重用这套机制,因为它便宜,而且它聪明。yank 是一根公开、即时、只要一行的杆子, 能让一个注册表去建议升级,而每个生态都有一根。跑你构建的那个东西会看到那条建议并照着做,而 如今照着做的那个东西,往往缺了整整一个周四早上的上下文,不知道哪个 crate 是哪个。
所以把这个决定放到不需要上下文的地方。一个版本发布与你的构建之间隔着两天的日光,不是对某个 包、某位维护者,或某个只差一个字母的账号名字下的判断。那是一只钟,而这场行动只有九十分钟。
来源:Rust Security Response WG“Supply chain attack on arrayref”(2026 年 8 月 20 日) · Wiz“Rust Supply Chain Attack on arrayref: Significant Overlap with DPRK Campaigns”(2026 年 8 月 20 日) · StepSecurity“arrayref, internment, and append-only-vec Poisoned by the proc-macro1 Build-Time Dropper”(2026 年 8 月 20 日) · The Hacker News“Rust Supply Chain Attack Puts Build-Time Malware in Crates with 245 Million Downloads”(2026 年 8 月 20 日) · BleepingComputer“Hackers poison arrayref Rust crate to push infostealer malware”(2026 年 8 月 20 日) · Semgrep“Rust crates arrayref & append-only-vec compromised”(2026 年 8 月 20 日)