Trusting-Trust 攻击首次端到端实现:一个 strip 工具攻陷整个 Linux 发行版

1984 年,Ken Thompson 在图灵奖讲座《Reflections on Trusting Trust》中描述了一个构造:被篡改的编译器在编译目标程序时植入后门,编译自己的源码时把同样的逻辑写进下一代编译器。此后源码可以完全公开、任何人可审,后门仍随每一次重新编译传递下去。这个构造通常被归为编译器特有的威胁,也长期停留在思想实验层面:恶意编译器需要识别自己的源码,源码一旦大幅演进,传播链条就可能中断。

2026 年 7 月 27 日,Julien Malka 等五位研究者提交 arXiv 的论文《Trusting-Trust Attack against an Entire Linux Distribution through Binary Manipulation》(arXiv:2607.24888,DOI: 10.48550/arXiv.2607.24888)给出了第一个有完整记录的端到端实现。载体选了 GNU binutils 里的 strip 工具:它不读源码、不生成代码,只在软件构建的最后阶段处理编译完成的 ELF 可执行文件。攻击在 NixOS 发行版的真实引导流程中传播,最终一份图形安装镜像里 3,791 个可在命令行调用的可执行文件中,3,790 个携带载荷。

Trusting-Trust 攻击传播链示意

Thompson 攻击的原始构造

Thompson 的构造分两层。第一层,恶意编译器识别出安全性敏感的目标程序(比如 login),在编译产物里插入后门。第二层,编译器识别出自己的源码,把前两层逻辑一并写进新编译出的编译器。两层都发生在编译阶段,源码本身干净。审计者读到的是无懈可击的代码,运行的是带后门的二进制,两者之间的偏差没有任何源码层面的痕迹。

这个构造的弱点同样明显:传播依赖编译器在源码里认出「自己」。四十年里它更多被当作理论边界的注脚,公开文档中缺少在生产环境跑通的完整实现。论文把这一步补上了,而且换了载体。

为什么是 strip

NixOS 的包管理器 Nix 用「derivation」描述每个包的构建环境和步骤,所有包从一套标准环境(stdenv)构建。stdenv 的构建存在自举问题:编译工具链本身需要编译器,所以发行版从一个预编译二进制集合(bootstrap seed)出发。这个种子捆绑约二十个预构建程序,包括 C 编译器、binutils、bash、coreutils、tar、gzip、patch 等。引导过程分多个阶段逐层从源码重建这些工具,直到最终标准环境不再依赖种子。

论文给自传播载体列了三个条件。C1:工具必须在种子里,先于自身源码被构建而被信任。C2:旧一代工具必须能写新一代工具的二进制。C3:被写入的新二进制日后要接替同样的角色(论文称为 successor edge),载荷才能跨代传递。strip 三个条件全部满足:它随 binutils 在种子里;Nix 构建每个包的最后有个 fixup 阶段,默认用 strip 清理安装产物;strip 自身重建时也要经过 fixup,被上一代 strip 经手。

还有一个选型理由:gcc 只影响它编译的语言,strip 处理的是成品二进制,覆盖发行版产出的所有语言生态。

三种 ELF 变换:不碰一行源码

整个注入通过三个只做加法的 ELF 文件变换完成,原有程序字节一个都不改:

  1. Append(追加):把载荷字节追加到 ELF 文件末尾。此时该区域未被 program headers 引用,不映射进内存,程序行为无变化。
  2. Repurpose(挪用):把一个闲置的 PT_NOTE program header(加载器不需要它来执行程序)改成 PT_LOAD,指向文件末尾新追加的载荷,使其成为可执行内存段。工具链二进制默认带 .note.gnu.build-id note,所以总有一枚可挪用的 PT_NOTE。
  3. Redirect(改写):修改 ELF header 的 e_entry 字段,内核启动程序时直接跳进载荷。载荷内嵌原始入口地址,执行完自己的动作后跳回原入口,宿主程序照常运行。

ELF 文件有两套并行的描述视图:加载器读的 program headers 和工具读的 section headers。nixpkgs 的后处理会按 section 表裁剪文件,所以注入器还把载荷登记为新的 .payload section,在文件末尾另建一张扩展 section 表,再把 e_shoff 指向新表。旧表留在文件里但不再被引用。

载荷如何生存

载荷被塞进依赖关系毫无共同点的各种可执行文件,所以它不链接任何库,绕开 libc 和宿主动态符号,直接调用内核,写成 freestanding 程序。它在普通 _start 例程之前运行,保存内核移交的寄存器、按 x86-64 调用约定对齐栈,做完动作后原样恢复,再跳到宿主原始入口。进程的 PID、文件描述符、信号上下文全部保持原状。

两个环境检测决定它的行为。读 NIX_BUILD_TOP 环境变量判断是否处于 Nix 构建沙箱内,沙箱内保持静默,避免在构建期触发任何功能测试失败;读自身 basename,只在宿主程序真的是 strip 时执行注入逻辑。原型载荷的实际动作是在标准输出打印一行「You've been pwned!」,论文说明这个标记、.payload section 名、basename 检查都是实验观测手段,替换成凭据窃取等行为不需要改变传播结构。

引导链上的传播

种子里的 strip(记作第 0 代)在每个包的 fixup 阶段经手所有新构建的二进制。binutils 在引导中重建不止一次:当新一代 strip 从干净源码编译完成、等待 fixup 时,上一代 strip 已经先经手了它,把载荷种进去。于是每一代 strip 都带着载荷,链条一路传递到最终标准环境里的 strip。此后每个包构建到 fixup 阶段,被污染的 strip 都会顺手注入。攻击因此越过了 Thompson 构造的经典障碍:它识别的目标与源码无关,源码怎么演进都不影响传播。种子离开依赖闭包后,攻击照常存续。

三种 ELF 变换

实验结果

论文在 nixpkgs 修订 fef9403a3e4d(GNU binutils 2.44,x86_64-linux)上评估,目标是图形 ISO 安装镜像,约 2,000 个包。

指标数值
构建闭包含包输出1,199 个
可调用的 ELF 可执行文件3,799 个
其中 CLI 可调用3,791 个
调用时打印感染标记3,790 个
唯一漏网firefox 147.0.3
闭包总大小6.16 GB
包大小中位数0.39 MB

构建零失败,完整图形镜像正常产出。漏网的 Firefox 原因明确:它的构建配置显式传了 --disable-strip --disable-install-strip,整个构建过程没有让 strip 碰过它的二进制。感染覆盖 C/C++(bash、git、sudo、curl)、Python(python3.13、pydoc3.13)、Rust(rsvg-convert)、Go(captree)、Lua(lua),印证了攻击对语言生态无差别。论文还跑了 NixOS 图形镜像对应的虚拟机功能测试套件,gnome-shell、mutter、nautilus、GNOME 控制中心全部由被污染的 strip 构建,没有一条测试失败。

git 二进制 56,313 KB,python3.13 10,928 KB,bash 1,327 KB,全部照常工作,只是每个程序启动时先执行了一段不该存在的代码。

检测与对抗

这次实现的直接含义是:读源码、审配方、对单次构建做产物证明(如 in-toto),都检测不到这类攻击。载荷不携带任何恶意源码,构建步骤本身按配方诚实执行,恶意逻辑只存在于二进制代际之间。

现役对抗手段分三档。可复现构建(reproducible builds)让同一源码产出字节一致的二进制,代际差异会暴露注入。Wheeler 的 DDC(Diverse Double-Compiling)用可信编译器重编译并比对。最彻底的是 Guix 2023 年落地的 full-source bootstrap:把引导种子压缩到几百字节,整条工具链从可审计源码逐层重建。论文在讨论中给这一路线补了一个新检查项:full-source bootstrap 只有把 strip、patchelf 这类构建后处理工具也纳入从源码重建的范围,才算封死载体;只重建编译器不够,因为 Thompson 式传播已经不需要编译器。

候选载体排查结果也写进了论文:strip 和 patchelf 满足全部三个条件;install 和 cp 在 make install 路径上有传播边但依赖各包 Makefile,属可能目标;make、sed、grep、patch、tar、gzip、xz、ln、rm、mkdir 不重写程序二进制,无法承载。

威胁模型与边界

攻击者的能力要求只有一条:替换引导种子里任意一个可执行文件。实现路径包括攻陷种子的构建或发布系统、推动一次被合并的种子更新,或者对种子审核流程做社会工程。论文的威胁模型里攻击者改不了任何包配方和源码,这条约束让攻击完全隐形于包级审计。

原型有意做得显眼。真实攻击可以把传播限制在 strip 自身的代际之间,其余时间完全休眠,等到抵达最终目标再激活,时滞可以拉到以年计。当前注入器也有边界:只支持小端 ELF、带闲置 PT_NOTE、ET_EXEC 或带解释器的 ET_DYN 二进制;共享库、其他架构、无解释器的静态 PIE 都在范围外。论文判断这些是工程复杂度的取舍,模式本身没有架构障碍。

把它放进供应链攻击谱系里看:SolarWinds 的 SUNSPOT 在构建时改写源文件,xz 后门藏在构建机制里,Shai-Hulud 蠕虫在 npm 包之间水平传播恶意源码。这几类攻击要么留下可审查的恶意源码,要么重建即清除。这次实现的传播发生在构建过程内部、二进制代际之间,不携带恶意源码,种子移除后依然存活,源码审查和逐产物证明两类防线同时失效。1984 年那个构造的适用范围,从这一版起需要改写成「编译器以及一切会改写二进制的构建工具」。

论文的完整实现与实验材料以 replication package 形式公开(figshare),作者在结语中把结论压缩成一句话:trusting trust 攻击不限于编译器,GNU strip 这样一个既不读也不写源码的普通构建工具,足以把整条攻击链在真实发行版上跑通。

来源:

  • 论文:arXiv:2607.24888
  • Thompson, K. (1984). Reflections on Trusting Trust. Communications of the ACM, 27(8), 761–763
  • Replication package:figshare 公开存档