我用 Rust 不多,但 cargo 这个包管理器的生态算是比较成熟的,社区对安全性也一直很自信。结果这次被打了个措手不及。
整个事件大概是这样:有人通过一个被入侵的维护者账号,往 crates.io(Rust 的 npm)上传了一个恶意版本的 arrayref crate。这个包可能你没听过,但如果你用了 egui、iced 这些 GUI 框架,或者 blake3 哈希库,甚至以太坊和 Solana 的某些组件------你的项目依赖树里大概率有它。 arrayref 累计下载 2.45 亿次,光过去 90 天就 5300 万。
攻击是怎么发生的
昨天凌晨(UTC 时间 8 月 20 日 01:17),攻击者先注册了一个 GitHub 账号 dtolney。 这个名字,你要是不仔细看,可能觉得就是 dtolnay------Rust 圈子里非常知名的开发者,proc-macro2、serde 这些核心库的作者。注意区别:真的叫 dtolnay,假的叫 dtolne**y,最后两个字母反了。
注册完号,攻击者用这个身份发布了一个叫 proc-macro1 的 crate。注意,不是 proc-macro2,是 proc-macro1。而且这个包的源码,就是从真正的 proc-macro2 原封不动抄过去的------连文档里的链接和 issue 引用都一并复制了,build 不会有任何异常。 但 build script 里,藏着东西。 五个小时后,攻击者动手了。他们劫持了 arrayref 维护者 droundy(David Roundy,另一位资深 Rust 开发者)的 crates.io 账号,用他的权限发布了三个恶意版本:
arrayref@0.3.10internment@0.8.7append-only-vec@0.1.9
这三个包在 Cargo.toml 里各加了一行依赖,指向那个假的 proc-macro1。 然后,最骚的操作来了------攻击者在同一个账号下,把 arrayref 所有之前的正常版本(0.3.5 到 0.3.9)全部 yanked 了。 yanked 是 crates.io 的一个机制,类似于"标记为已撤回"。Cargo 在解析依赖时,会尽量不用 yanked 的版本,还会弹一句 "consider updating to a version that is not yanked"。
好家伙,这一手直接把升级路径给堵死了。旧版本全被标记,唯一没被 yanked 的就是恶意版本 0.3.10。Cargo 的"建议更新"提示,直接变成了攻击者的引流工具。发现这个漏洞的研究员 jhobern 自己就是被这个提示引过去的。
build.rs 里到底写了什么
Cargo 有个设计:每个 crate 可以有一个 build.rs 构建脚本,在编译之前自动执行。这个设计的初衷是做 C 库链接、代码生成之类的事情。 问题是,build.rs 的执行权限是完整的系统级,跟你手动跑一个 shell 脚本没区别。 proc-macro1 的 build.rs 里,攻击者把 C2 服务器地址拆成了几段 base64 碎片:
less
const SRC_URL_PARTS: &[&str] =
&["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];
拼起来解码后是 https://23.254.165.112:9089/。然后脚本禁用 TLS 证书校验(自定义了一个 AcceptAll 验证器,所有检查直接 return Ok),下载对应平台的二进制文件:
- Linux x86_64 →
rust-crate_0.1.0 - Windows x86_64 →
rust-crate_0.2.0 - macOS x86_64 →
rust-crate_0.3.0 - macOS aarch64 →
rust-crate_0.4.0
在 Unix 上,恶意文件写到 /tmp/rust-setup,chmod +x,然后 detached 启动。在 Windows 上更有意思------写了一个 PowerShell 脚本,再通过 VBScript 启动,故意绕过 Cargo 的进程树管理。源码里甚至写了一行注释解释为什么用 WScript:
arduino
// ShellExecute via WScript escapes Cargo's job object; spawned children otherwise
// keep the build script (and `cargo build`) waiting until they exit.
然后还来了个 std::mem::forget(child),直接泄漏进程句柄,确保恶意进程完全脱离编译进程独立运行。 我读到这里的时候说实话有点发毛。这代码写得相当专业,不是随手糊的脚本小子。 恶意程序跑起来之后干什么呢?连接 C2 服务器,然后偷浏览器密码。Chrome、Brave、Edge 的 SQLite 登录数据库全部扫一遍。接着在系统里装持久化后门:Windows 写注册表 Run key,macOS 放 LaunchAgent,Linux 建 systemd 用户服务。 说白了,就是冲着你浏览器里存的那些密码来的。
这个攻击为什么这么阴
有几点我觉得值得说说。
第一,攻击者非常有耐心。他们没有直接搞一个 typosquat 碰运气,而是先准备了几乎以假乱真的 proc-macro1(源码直接复制 proc-macro2),然后等到维护者账号被拿下之后,通过正规渠道发布,再 yank 掉所有旧版本来"逼"用户升级。这整套操作至少提前了好几个小时开始准备。
第二,Rust 的 build.rs 机制在生态内一直被视为"安全的"。不像 npm 的 postinstall 脚本大家都默认可能有猫腻,Rust 开发者普遍不会去怀疑一个构建脚本。但 build.rs 本质上就是一个有完整系统权限的可执行代码。
第三,yanked 机制被反向利用了。这个机制原本是为了让维护者"撤回"有问题的版本,防止新用户使用。攻击者反过来操作:把好的全 yanked,只留一个恶意的。Cargo 的警告反而成了攻击者的帮手。
如果你在用 Rust 项目,并且你的依赖树里有 arrayref(通过 tiny-skia、sctk-adwaita、winit 等间接依赖概率很高),那昨天的 cargo build 有可能直接让你的开发机裸奔了。
谁干的
目前没有人认领。 Wiz 的分析指出,这次攻击使用的基础设施与朝鲜(DPRK)关联的黑客活动有大量重叠。C2 地址 23.254.165.112 出现在此前朝鲜发起的 Mastra 行动中,还有一个 IP 在分析朝鲜 npm 攻击的报告里见过。攻击者都用的 Hostwinds 的 IP 段。 不过目前没有任何组织被正式指控。
影响范围
被波及的项目包括:
- blake3------高性能哈希库
- egui、eframe、iced------Rust 生态主流 GUI 框架
- 以太坊和 Solana 的部分组件
好消息是攻击窗口极短。arrayref@0.3.10 在 07:15 UTC 发布,08:41 被删除,只活了 86 分钟。append-only-vec@0.1.9 稍微长一点,107 分钟。Nextron Systems 的研究员发现后第一时间报告了 Rust 安全响应团队,响应速度算快了。
检查自己是否中招
如果你昨天有 Rust 项目在 build,建议赶紧查一下。 先翻 Cargo.lock,看有没有这三个版本:
csharp
grep -E 'arrayref.*0.3.10|internment.*0.8.7|append-only-vec.*0.1.9|proc-macro1' Cargo.lock
再扫一下 cargo 缓存:
rust
find ~/.cargo/registry/cache -type f \
-name 'arrayref-0.3.10.crate' -o \
-name 'internment-0.8.7.crate' -o \
-name 'append-only-vec-0.1.9.crate' -o \
-name 'proc-macro1-*.crate' \
-print
如果确认 build 了恶意版本:
- 别慌,但也别拖。从这台机器能访问到的所有密码、token、SSH key,全部轮换
- 查
/tmp/rust-setup是否存在(Unix),查%TEMP%\rust-setup.ps1和%TEMP%\rust-setup-launch.vbs(Windows) - 检查持久化:Windows 看注册表 Run key,macOS 看
~/Library/LaunchAgents/,Linux 查systemctl --user list-units - 最好从干净备份重建开发环境
如果没中招,也建议锁定安全版本:arrayref 锁定到 0.3.9 或更早。
写在最后
这几个月供应链攻击密度明显在升。年初的 xz-utils 后门事件,8月初的 npm Keyv 供应链攻击,现在又是 Rust 的 arrayref。攻击者的手法越来越精细,越来越有耐心。 xz 那次是国家级力量花了好几年渗透,npm Keyv 那个是 typosquat 加上恶意 build 脚本,这次的 Rust 事件更狠------直接在维护者账号上动手,还用 yanked 机制来"逼"用户上钩。
有几个事情我觉得应该成为标配了:lock 文件进代码审查流程,CI 环境隔离做到跟生产环境一个标准,构建前跑一遍 cargo audit(npm 对应的是 npm audit)。 还有 build script 这个东西。npm 生态里大家已经习惯了 postinstall 可能有问题,但 Rust 的 build.rs 还没建立起同样的警惕。这次之后,怕是得重新评估一下了。