2026 年 8 月 20 日早晨,Rust 生态中累计下载 2.45 亿次的 crate 发布了一个只存活了数小时的新版本。arrayref 全部内容只有四个宏、327 行源码,但它通过 tiny-skia、winit 这条依赖链埋在 egui、iced 等主流 GUI 框架项目的依赖图底部。07:11 UTC,名为 dtolney 的账号发布了 proc-macro1 1.0.107;四分钟后,droundy 账号发布 arrayref 0.3.10,依赖清单里多出一行指向 proc-macro1 的条目。约 40 分钟后 RustSec 收到报告,当天 crates.io 移除了这两个恶意包,GitHub 上 droundy 的全部仓库返回 404。

入侵点:一行 manifest 依赖
arrayref 0.3.10 的宏源码与 0.3.9 完全一致,恶意内容全部集中在 Cargo.toml 新增的一个条目:
[dependencies.proc-macro1]
version = "1.0.107"Cargo 会构建清单中声明的所有非可选依赖,与源码是否引用它们无关。这一条目足以让任何拉取 arrayref 0.3.10 的项目在编译时自动获取并执行 proc-macro1 的构建脚本,攻击者一行宏代码都不需要改动。
version = "1.0.107" 是一个 caret 范围。proc-macro1 只发布过两个版本,解析结果必然落在恶意的 1.0.107 上;早五小时上传的 1.0.106 是一份干净的 proc-macro2 副本,作用是把版本号铺垫到位。
proc-macro1:proc-macro2 的改名复制
proc-macro1 的 src/ 目录是 proc-macro2 的机械替换版本,把字符串 proc-macro2 全局改为 proc-macro1,连文档链接和 issue 编号都原样保留。库代码是真的,构建照常通过,调用方几乎无法察觉差异。
包元数据伪造了身份:authors 字段写着 David Tolnay 的名字和一个不属于他的邮箱,repository 指向 dtolnay/proc-macro1,该地址返回 404。发布者用户名 dtolney 与真实的 dtolnay 只差一个字母,属于典型的 typosquat。
暴露意图的位置在构建依赖里。一个 token 解析库的 build-dependencies 中出现了 base64、rustls 和 ureq:解码、TLS 栈、HTTP 客户端三样凑齐,说明构建脚本需要访问网络。
build.rs:编译期的下载与执行
载荷藏在 proc-macro1 1.0.107 的 build.rs 里。服务器地址被拆成 base64 分片,编译时重组,完整地址从不直接出现在源码中:
const SRC_URL_PARTS: &[&str] =
&["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];
const END_URL_PARTS: &[&str] =
&["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"];解码后分别是载荷主机 https://23.254.165.112:9089/ 与 C2 地址 23.254.165.112:443。TLS 客户端使用 AcceptAll 证书校验器,全部校验方法无条件返回成功,自签证书的裸 IP 端点也能通过握手。下载目标按操作系统与架构四选一:linux/x86_64 取 rust-crate_0.1.0,windows/x86_64 取 _0.2.0,macos/x86_64 取 _0.3.0,macos/aarch64 取 _0.4.0,其余平台直接 panic 终止构建。
执行阶段分平台处理。Unix 下写入 /tmp/rust-setup,标记可执行后以 detached 方式 spawn,三个标准流全部指向 null,C2 地址作为 argv[1] 传入。Windows 下写出一个 PowerShell 脚本和一个 VBScript 启动器,经 wscript.exe 以 CREATE_NO_WINDOW 隐藏执行。源码注释写明走 WScript 是为了逃出 Cargo 的 job object,最后的 std::mem::forget 泄漏子进程句柄,让载荷在构建结束后继续存活。
yank 诱导:把升级指向唯一选项
分发侧还有一步诱导。呈现被接管特征的 droundy 账号将 arrayref 0.3.5 至 0.3.9 全部 yank。Cargo 对 yanked 版本会打印 "consider updating to a version that is not yanked" 警告,开发者按提示升级时,唯一未 yank 的选项就是 0.3.10。RustSec 报告者正是在处理这个警告时装上了恶意版本。
清理完成后,crates.io 移除了 0.3.10,0.3.9 恢复为最新可用版本,版本列表回到正常状态。同属 droundy 的 append-only-vec 0.1.9 也被标记为同一 actors 的可疑发布。
影响面与自查
arrayref 通过 tiny-skia、sctk-adwaita、winit 传导,位于 egui、eframe、iced 等框架的依赖图下方。crates.io 数据显示该 crate 累计下载 2.45 亿次,最近 90 天 5390 万次,干净的 0.3.9 版本历史下载约 1.52 亿次。这些数字衡量的是 crate 的覆盖广度,实际受影响的构建集中在 8 月 20 日当天拉到 0.3.10 的机器上。
事件的关键 IOC:
| 类型 | 指标 |
|---|---|
| 网络 | 23.254.165.112:9089(载荷主机) |
| 网络 | 23.254.165.112:443(C2) |
| 文件(Unix) | /tmp/rust-setup |
| 文件(Windows) | %TEMP%\rust-setup.ps1、rust-setup-launch.vbs |
| 第二阶段 | rust-crate_0.1.0 至 _0.4.0 |
被移除构件的 SHA256 指纹:arrayref 0.3.10 为 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae;proc-macro1 1.0.107 为 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4;1.0.106 为 b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436。
自查从 Cargo.lock 开始:
grep -E 'name = "(arrayref|proc-macro1)"' -A 1 Cargo.lock命中 version = "0.3.10" 的 arrayref 或任何 proc-macro1 条目,说明构建拉过恶意版本,需按上表检查 /tmp 与 %TEMP% 下的文件及到 23.254.165.112 的出站连接。RustSec 数据库已收录对应条目(advisory-db #3161),更新数据库后 cargo audit 与 cargo deny check advisories 均可报出该漏洞。
build.rs 与构建进程同权限,能读取环境变量、SSH 密钥与凭据文件,依赖清单里的一行新增就是它的执行入口。检查 Cargo.lock,是当天拉过新依赖的 Rust 项目最直接的自查动作。