云厂商软件供应链安全三面:恶意 crate 与 build.rs 防护
- 轮次
- 三面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 有个恶意 Rust crate 在 build.rs 里嵌入了构建时 payload,你了解这个 case 吗?说说你的理解,以及如果你是企业安全负责人会怎么防。
- 这个攻击链跟普通的恶意包有什么不同?
- Rust 不是号称比 C/C++ 内存安全吗,怎么还会出这种问题?
- 你具体怎么设计防护体系?
- 大型互联网公司可能有几万个 crate 依赖,每个都人工审核不现实吧?审核怎么分级?
- build.rs 没有官方的沙箱机制,如果 Rust 官方不解决,企业应该自己造轮子还是等社区?
- 如果已经中招了,怎么发现?
- 你觉得 Rust 生态在供应链安全上,跟 npm 和 PyPI 比处在什么阶段?
《参考解析》
build.rs 的信任模型:Cargo 在编译 crate 时会自动执行同目录下的 build.rs,且与编译进程同权限运行——能读写文件系统、发起网络请求、执行系统命令、读取环境变量。它本来的用途是链接系统库(println!("cargo:rustc-link-lib=..."))、生成绑定代码(bindgen)、编译 C 代码(cc crate,比如 OpenSSL 的 openssl-sys),所以社区默认它是可信的。关键在于这个信任是无条件的:Cargo 不做沙箱隔离、不在执行前弹确认、也没有类似 npm --ignore-scripts 的开关。因此只要你在 Cargo.toml 里写了一行依赖,它的构建脚本就一定会在 cargo build 阶段跑起来,时机早于你的程序启动——这跟传统「程序运行后触发」的恶意包是完全不同的攻击面。回答这题要把「谁执行、以什么权限执行、什么时候执行、有没有开关」四点讲全,这是整个 case 的技术地基。
为什么说这个攻击链是升级版:三层差异。第一层是执行点可信:build.rs 是构建系统的合法执行入口,不是漏洞利用,不需要绕过任何防护,防火墙和运行时检测天然看不到它。第二层是攻击时机在构建期:payload 在 cargo build 时就已经跑完,而构建机通常握有代码仓库凭据、镜像仓库 push 权限、CI/CD token、云上临时凭证,拿下构建机等于拿下整条交付链——所以「生产环境运行时防护很严」并不能救你,被打穿的是研发基础设施。第三层是投毒手法:不改知名库的源码,而是注册名称高度相似或使用同形异义字符的包名(typosquatting),在 Cargo.toml 里手敲依赖时肉眼极难分辨。这条真实存在:2022 年 crates.io 上就出现过仿冒 rust_decimal 的 rustdecimal,由 crates.io 团队与 Rust Security Response WG 下架;教训是注册表的名称相似度校验不能只靠平台侧。另外要能对比说明:npm 生态被毒打过很多轮,至少给了 --ignore-scripts 这种降低风险的手段,Rust 这边目前主要靠第三方工具和构建侧隔离。
五层防护体系与三级审核:第一层依赖准入,维护内部 allowlist 镜像源,第三方 crate 必须过审才能同步进来,开发者只能从内部镜像拉取而不直连 crates.io;审核项包括作者身份、包名与知名库的相似度(编辑距离 + 同形异义字符归一化)、源码审计(重点看 build.rs 与 unsafe 块)、下载量与发布历史合理性(昨天注册的包不可能有百万下载)。第二层构建环境隔离:CI 在一次性容器里构建,除内部镜像外无网络、文件系统只读、产物通过 volume 导出,seccomp 禁掉 execve/ptrace 等高危系统调用,AppArmor/SELinux 限制可写路径,容器销毁后恶意脚本什么都留不下。第三层构建时行为监控:用 eBPF 挂 tracepoint 跟踪构建进程的 connect()、openat()、execve(),正常构建脚本只做代码生成和写文件,一旦出现外联 DNS 或非内部地址的 HTTPS 连接就告警阻断。第四层来源可验证:SLSA 分级要求构建过程可追溯、产物可验证,配合 cargo-vet / cargo-crev 做社区审查背书,关键项目用 -Zbuild-std 从源码编标准库以免工具链被篡改。第五层开发者侧:内部 CLI 加 pre-commit hook,新增依赖时自动算与热门包的编辑距离并告警,定期做供应链演练(往内部镜像放蜜罐 crate 看谁引入)。几万个依赖不可能全人工看,所以要分三级:L1 机器审核(包名相似度、作者邮箱域名、下载量曲线、是否含 build.rs 与网络请求代码),90% 在这里过;L2 人工抽查 L1 标记可疑的包;L3 只对核心系统的关键依赖逐行审计。答这题的关键是把「自动化兜住大多数、人力只花在长尾」这个成本结构讲出来。
“自己造轮子还是等社区”怎么答:结论要明确:不能等。理由是企业攻击面今天就存在,而官方机制(build script 沙箱或 --ignore-build-scripts)即使推进也要跨版本、跨生态协调,时间不可控。可行的过渡方案有:用 Bazel 之类的构建系统替代 Cargo 做编排,Bazel 的 sandbox 天然限制每个构建动作的文件系统与网络可见性(Google 内部就是这个路子);在容器层自己做隔离(前面第二层);用 cargo-audit / cargo-deny 查已知漏洞与许可证,用 cargo-vet 做依赖背书;对少量关键依赖做 vendor 后再构建,避免每次构建都从公网拉。同时要承认这些方案的代价:Bazel 迁移成本高、vendor 会带来升级维护负担、只读文件系统会破坏一部分需要写缓存或生成代码的构建脚本,需要按仓库分批推进。
中招后的三个检测信号:第一,CI 构建日志或网络侧出现异常外联——正常 Rust 构建除了拉依赖不应该联网,构建过程中到未知 IP 的出站连接、异常 DNS 查询都应立刻告警(这也是 eBPF 监控的价值,能定位到哪个进程发起的)。第二,构建容器的文件系统 diff 出现非预期写入,尤其是往 ~/.ssh/、~/.aws/、~/.cargo/credentials、/etc/ 这类目录写文件,或修改 shell 启动脚本、git 配置。第三,构建产物里出现非预期的二进制与代码:一个纯计算库编出来的 .so 里包含网络通信符号,用 readelf -d、objdump -d、nm、strings 都能看出来;也可以对比产物哈希与上游可复现构建的结果。要补一句:检测的前提是留下可审计的基线——构建日志集中留存、镜像与产物有签名和哈希、依赖清单(SBOM)可查,否则事后根本说不清什么被改了。事后处置则包括轮换构建机上所有凭证、追查已发布产物、在内部镜像封禁该包及其作者。
Rust 生态处在什么阶段:npm 和 PyPI 被毒打过多轮,生态已经长出成熟工具链(Socket.dev、Snyk、Dependabot、npm audit、pip-audit),平台侧也有提审、下架、双因素强制等机制。Rust 目前的处境是认知刚从「我们不太可能出问题」转向「原来我们也会出问题」:cargo-crev 和 cargo-vet 是好的开始但普及率低,cargo build 默认无沙箱,名称相似度防护主要靠社区自觉。可以补充判断依据:Rust 的使用场景正在往基础软件、云原生基础设施、甚至内核与驱动扩散,一旦进入构建链条上游,单点投毒的杠杆效应比前端依赖大得多;所以更现实的期待是这次事件推动 Cargo 增加构建脚本沙箱或开关,而不是靠个别企业自建防御。答题时把「机制缺什么」和「生态治理缺什么」分开说,比单纯吐槽更有深度。