有一篇 arXiv 论文值得每个做构建系统的人读:《Trusting-Trust Attack against an Entire Linux Distribution through Binary Manipulation》,作者是 Julien Malka、Aman Sharma、Martin Monperrus、Stefano Zacchiroli、Théo Zimmermann,7 月 27 日提交(arXiv:2607.24888)。

它把一个被当成"理论威胁、且只跟编译器有关"的攻击,做成了在真实发行版上跑通的完整链条。

白纸黑字的前提被推翻了

Ken Thompson 1984 年那篇《Reflections on Trusting Trust》讲了一个思想实验:你写了一个干净的 C 编译器,但它被篡改过,会给它编译的程序植入后门,并且会把"篡改自己"这件事也保留到下一版编译器里。于是即使你审计了所有源码、用干净编译器重编,后门依然在。

这个攻击长期被视为两个限制条件下的威胁:它需要篡改编译器,而且它是思想实验(没人公开做过完整的)。

这篇论文推翻的是第一个限制——攻击不一定需要编译器。

攻击的核心:用 GNU strip

作者的切入点是 GNU strip,一个再普通不过的构建工具。strip 的功能是从 ELF 文件里删掉符号表和调试信息,它既不读源码也不生成源码,纯粹是对已完成的二进制文件做操作。

按理说,这类工具跟"在程序里植入后门"沾不上边。论文的贡献就是证明它可以。

做法是:只用对已完成的 ELF 文件的操作,构造一条完整的 trusting-trust 攻击链。

关键机制在于 strip 的特殊位置——它自己也是被构建出来的二进制,而且构建下一代 strip 的时候,用的是上一代 strip。这就给了攻击一个自我传播的载体:

  • 在一个被篡改的 strip 里植入一段载荷。
  • 这个 strip 在处理二进制时,把自己的载荷注入进去(比如在目标程序里插入一段逻辑)。
  • 同时,在用它去处理(下一代)strip 自己的时候,把载荷也写进新一代 strip。
  • 于是载荷一代一代传下去,不依赖源码,也不依赖编译器。

为什么它能在 NixOS 上跑通

论文的验证对象是 NixOS,这一步很关键,因为 NixOS 的构建流程有一个特殊的结构:二进制种子(binary seed)。

NixOS 的可复现构建是分阶段引导的。整个体系不是从源码一路编到底——它需要一个"起点",即一组预先编译好的二进制(种子),用它们来构建后面的东西。这个种子阶段的设计,是为了避免"要构建工具链得先有工具链"的无限递归。

论文描述的链条是:

  • 在二进制种子里篡改一个 strip。
  • 这个被篡改的 strip 会植入一个载荷。
  • 载荷从一代 strip 传播到下一代。
  • 关键一点:当种子离开依赖闭包之后,载荷依然存活,最终进入标准环境(stdenv)。
  • 在一个真实的 nixpkgs 修订版上,这个攻击成功构建出一个完整的图形化安装器,且没有任何构建失败,并且几乎给它的每一个二进制都植入了后门。

"没有任何构建失败"这句话的分量很重。它意味着攻击不是在某个环节被绕过的特例,而是整个构建流程如常运行、产出物看起来完全正常。

它推翻了哪些常见防御假设

这篇论文之所以重要,是因为它同时动摇了几个被广泛依赖的假设:

"可复现构建能挡住这个。" 可复现构建保证"同样的源码 + 同样的构建环境 = 同样的产出",它挡住的是构建过程被污染。但这个攻击的恶意行为来自被污染的构建工具本身,源码和配置都是干净的。可复现构建不能帮你发现"参与构建的那个二进制是坏的"——因为它每次都会复现出同样坏的结果。

"审计源码就够了。" 攻击不在源码里,在二进制里。你审计的每一行源码都是干净的,后门在编译好的 strip 里。

"这是编译器的问题。" 论文直接用 strip(一个不涉及源码的工具)证明了这个边界是假的。任何"被上游工具处理过的二进制"都可能是载体。

"二进制种子是可信起点。" 种子的意义正是"从这里开始信任"。而攻击恰恰选的这个点——种子被污染,后面全都不可信。

真正的启示:信任边界必须追溯到二进制

从防御角度,这篇论文给出的方向不是"如何检测这个具体的攻击",而是如何重新划定信任边界:

  • 构建工具本身必须被纳入信任评估。 不能只问"源码干不干净",还要问"处理这些源码的工具是二进制,它是哪来的、能不能独立验证"。
  • 引导路径(bootstrap)要尽量缩短、要有多路验证。 如果整个体系必须依赖一小撮预编译二进制,那这一小撮就是系统的信任根,需要最严格的来源审计——最好是多个独立实现的构建路径互相验证(diverse double-compiling 的思路),两条路径产出的二进制如果不一致,就说明有问题。
  • 种子阶段的来源要可追溯。 谁编译的种子、用什么编译的、有没有第三方复现过,这些不是形式主义的问题,是唯一能打破"信任传递"的办法。
  • Nix 的路径哈希有帮助但不够。 Nix 用内容哈希标识每个构建产物,这比"同名同版本"强得多,但它保的是"内容一致的传输",保不了"内容从一开始就是坏的"。

⚠️ 网络安全免责声明

本文内容仅供技术研究与安全学习之用。文中描述的漏洞分析、攻击链拆解和代码示例均基于公开披露的安全研究,旨在帮助开发者理解攻击原理、提升安全意识和防御能力。

请勿将本文所述技术用于任何未经授权的安全测试、渗透攻击或非法用途。任何因滥用本文信息而造成的法律后果,由使用者自行承担,与本文作者无关。

如发现系统安全漏洞,请通过合法渠道向相关厂商或平台报告,共同维护网络安全生态。

我的做法

在构建链安全上,这件事让我把关注点从"源码"扩到"二进制工具":

  • 构建环境里所有预编译的二进制(编译器、binutils、打包工具、种子),都当成"需要来源证明"的组件,而不只是"系统自带的东西"。
  • 尽量用多条独立构建路径交叉验证关键产物。单路构建永远无法自证清白。
  • 对自己的二进制分发物提供可复现的构建配方,让第三方能独立重建并比对。这是打破信任传递的唯一现实办法。
  • 关注 bootstrappable builds 这类从极小的、可人工审计的源头逐级构建的工作。它解决的就是"信任根到底能有多小"的问题。

Thompson 那篇四十年了,结论一直没变:信任不能靠源码的自证,只能靠对信任根的最小化和多重验证。这篇论文的价值是把这个抽象结论落到了一个具体的、能跑通的攻击上——包括那个"看起来完全正常的图形化安装器"。