← 返回首页

xz 后门复盘:源码干净,发布包带毒

供应链安全CVE-2024-3094Linux运维

2024 年 3 月 29 日,一位做 PostgreSQL 的开发者 Andres Freund 在 oss-security 邮件列表上发了一封报警信:Debian sid 上 ssh 登录突然开始吃满 CPU,valgrind 报了一堆以前没有的错。追下去发现,不是 Debian 的包被投毒,是上游 xz 项目的发布 tarball 被投毒了——而 git 仓库里的那份源码是干净的。

这件事最反直觉的地方,也正是它值得写一篇复盘的原因:你把源码从头到尾审一遍,看到的代码没有任何问题。问题在于你审计的那份源码,和构建系统实际吃进去的那份文件,不是同一个东西。

本文没有联盟链接,也不推荐任何付费产品,是纯技术复盘。文中每条命令与报错都来自官方公告、发行版安全公告或项目 issue 原文;我自己只实测了「检测脚本的匹配逻辑」这一层,并会明确标出这一层的边界(这台机器上没有 liblzma 可扫,我没有真实后门样本)。

TL;DR:受影响的是 xz 5.6.0 与 5.6.1 两个版本,CVE-2024-3094,CVSS 3.1 评分 10.0 CRITICAL,CWE-506。Debian 的回滚包版本号是 5.6.1+really5.4.5-1——看到这个 +really 就是要警觉的信号。危害路径是 sshd → libsystemd → liblzma,后门通过替换 ifunc 解析器改写 RSA_public_decrypt@...plt,攻击者持特定私钥即可在认证前执行命令。而真正的二次事故在修复端:5.6.2 移除了后门,同一天也因为用了 libtool-2.5.0 的一个 bug,导致 MIPS64 上共享库构建直接失败、--disable-static 静默失效。

谁受影响:三个发行版给了三种完全不同的处置口径

这件事当时没有统一的「全球受影响名单」,因为影响面取决于两个条件同时成立:装了 5.6.0/5.6.1,且 sshd 走 systemd 通知路径。下面是各发行版官方公告里写明的处置方式,都取自原始公告而非二手报道。

发行版受影响范围官方处置来源
RHEL(企业版)不受影响无需动作Red Hat 公告明确写「No versions of RHEL are affected」
Debian stable不受影响无需动作DSA 5649-1 写明「no Debian stable versions are known to be affected」
Debian testing / unstable / experimental5.5.1alpha-0.1(2024-02-01 上传)至 5.6.1-1回滚到上游 5.4.5 代码,包版本号 5.6.1+really5.4.5-1DSA 5649-1(2024-03-29)
Fedora 40含 xz-libs-5.6.0-1.fc40.x86_64.rpm 与 xz-libs-5.6.0-2.fc40.x86_64.rpm降级到 5.4.x,公告给了 Bodhi 变更号 FEDORA-2024-d02c7bb266Red Hat 2024-03-30 更新公告
Fedora Rawhide5.6.0 与 5.6.1官方要求「立即停止使用」,等回滚后再部署Red Hat 公告
Arch Linuxxz 5.6.0-1升到 5.6.1-2发行版公告汇总
Alpine(edge)5.6.1-r0、5.6.1-r1升到 5.6.1-r2发行版公告汇总

三个细节值得单独拎出来。

其一,Fedora 40 的定性是「装了有问题的包,但没证据表明已被利用」——Red Hat 在 3 月 30 日的补充说明里写得很谨慎:xz-libs-5.6.0-1.fc40.x86_64.rpm 和 xz-libs-5.6.0-2.fc40.x86_64.rpm 两个包都中了,但「Fedora Linux 40 does not appear to be affected by the actual malware exploit」。这类「包有毒但利用链没通」的中间态,在事故复盘里比单纯的「中招 / 没中招」二分法更有信息量。

其二,RHEL 与 Debian stable 都没事,但原因不同。RHEL 的公告给的结论很干脆,而 Debian 的原因写在 DSA 里:受污染的版本只进了 testing、unstable、experimental。发 stable 分支这件事本身,就是一道防线。

其三,Gentoo 是「拉了有问题的包但也没事」,原因在它自己的 issue 里说清楚了:Gentoo 的 OpenSSH 没有打 systemd-notify 的补丁,而后门依赖这条链路。我此前写过一篇 GitHub Actions 供应链投毒复盘,讲的是「怎么锁定第三方 action 的版本」,那是依赖锁定层;本文讲的是「你锁了版本也没用,因为发布产物本身可以跟源码仓库对不上」,属于产物溯源层,两件事的排查动作完全不重叠。

为什么审源码看不出来:git 仓库和发布 tarball 不是同一份文件

这是整件事的技术核心,也是它能潜伏两年多的原因。

Freund 在原文里指出,后门的一部分只存在于分发的 tarball 里。他给出的具体位置是 build-to-host.m4 的第 63 行,并附了 Debian 仓库的导入副本做对照:

https://salsa.debian.org/debian/xz-utils/-/blob/debian/unstable/m4/build-to-host.m4?ref_type=heads#L63

他的原话是:这一行在上游的 build-to-host 源码里并不存在,build-to-host 在 xz 的 git 仓库里也根本没被使用,「it is present in the tarballs released upstream, except for the "source code" links」。他特别点出了 GitHub 上那个「source code」链接是直接从仓库内容生成的,所以点进去看到的代码是干净的——这正是审计者最可能去看的地方。

Red Hat 的公告从另一个角度印证了同一件事:恶意代码被混淆后只完整存在于下载的包里,git 分发缺少触发构建的 M4 宏;但二阶段产物在 git 仓库里是有的,以便 M4 宏存在时能被注入。

也就是说,攻击分成了两半:

Freund 还特别指出,这两个文件在 5.6.0 里「were not even used for any "tests"」。也就是说,它们在仓库里的身份是「无害的测试夹具」,任何人做代码审计时看到它们都会跳过去。

混淆脚本解开后做的事,是改写 $builddir/src/liblzma/Makefile,把一个本该跑测试的目标替换成解压并执行载荷的管道。Freund 贴出的还原片段是这样的:

sed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1

解混淆之后是一串 head -c、tr、xz -d、/bin/bash 拼起来的命令,最终从 tests/files/good-large_compressed.lzma 里读出注入的代码。

后门到底挂在哪:一条从 libsystemd 到 sshd 的链

理解这条链,才能理解为什么「装了受影响版本的机器上,大多数其实没被攻破」。

后门代码最终替换了 ifunc 解析器 crc32_resolve() 与 crc64_resolve(),改为调用注入的 _get_cpuid()。按 Freund 的分析,这两个函数在启动早期被解析(因为 sshd 是用 -Wl,-z,now 编译的,所有符号会被提前解析),第一次调用时只是给一个 completed 变量自增,看起来完全正常。

关键动作在第二次解析:后门在动态链接器里装了一个 audit hook,监视 RSA_public_decrypt@...plt 这个符号被解析的时刻,一出现就把它的 GOT 条目改指向自己的代码。Freund 用 perf 抓到的痕迹是 pubkey 登录期间 sshd 从自身跳进了 liblzma:

sshd 1736357 [010] 714318.734008:  1  branches:uH:  5555555ded8c ssh_rsa_verify+0x49c (/usr/sbin/sshd) =>  5555555612d0 RSA_public_decrypt@...+0x0 (/usr/sbin/sshd)

然后再跳回 libcrypto 做正常认证:

sshd 1736357 [010] 714318.734009:  1  branches:uH:  7ffff7c137cd [unknown] (/usr/lib/x86_64-linux-gnu/liblzma.so.5.6.0) =>  7ffff792a2b0 RSA_get0_key+0x0 (/usr/lib/x86_64-linux-gnu/libcrypto.so.3)

这段发生在认证之前的上下文里,所以它的严重性来自位置,不是来自代码做了什么。这也是 CVSS 3.1 打出 AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H 的原因:网络可达、无需权限、无需用户交互、跨信任边界。

为什么大多数装了 5.6.0 的机器没被打穿

Freund 列出的触发条件非常具体,这些条件同时成立才会走到利用路径:

条件原文表述
TERM 环境变量未设置a) TERM environment variable is not set
argv[0] 必须是 sshd 路径b) argv[0] needs to be /usr/sbin/sshd
LD_DEBUG 与 LD_PROFILE 未设置c) LD_DEBUG, LD_PROFILE are not set
LANG 已设置d) LANG needs to be set
调试环境会被探测e) Some debugging environments, like rr, appear to be detected. Plain gdb appears to be detected in some situations

还有一条构建期门槛:目标必须是 x86_64 + linux-gnu,用 gcc 加 GNU ld 构建,并且是在 Debian 或 RPM 包构建流程里跑的(test -f "$srcdir/debian/rules" 或 RPM_ARCH 为 x86_64)。他判断最后这条大概率是故意加的,目的是让调查者更难复现。

一个能直接用的判别方法

Freund 给了一个非常实用的对比实验,不需要真的被攻破就能看出区别:

slow:
env -i LANG=C /usr/sbin/sshd -h
(about 0.5s on my older system)

fast:
env -i LANG=C TERM=foo /usr/sbin/sshd -h
env -i LANG=C LD_DEBUG=statistics /usr/sbin/sshd -h
(about 0.01s on the same system)

同一台机器,只改环境变量,耗时从 0.5 秒掉到 0.01 秒。他也在真实登录上测过,real 从 0m0.299s 变成 0m0.807s。而 LD_BIND_NOT=1 会让后门不生效——因为他分析出的原因之一是 GOT 在 -Wl,-z,relro 之后才被改成只读。

这套「拉高延迟」的副作用,恰好成了 Freund 当初发现问题的线索。换句话说,这个后门为了躲避分析做了防检测措施,而这个措施本身制造了一个可观测的性能特征。

我实测的部分:检测脚本的匹配逻辑,以及一个我自己踩到的误报

这一节要先把边界说清楚:我实测的是检测脚本的匹配逻辑,不是真实后门样本。 这台机器是 Windows,没有可扫描的 liblzma,所以我没有、也不打算假装有真实的被投毒二进制。

我实测的是:把公开分析里给出的特征串写成一个最小扫描器,然后喂给它构造的样本,看它会不会误判。用的特征串有三个,全部见于原始分析——yolAbejyiejuvnup=Evjtgvsh5okmkAvj(后门的一个 kill switch 环境变量名)、Shutdown、liblzma_la-crc64-fast(注入产物文件名)。

构造一个命中样本,两个特征串都被正确匹配:

=== 逻辑验证 1:构造「命中」样本 ===
命中: ['yolAbejyiejuvnup=Evjtgvsh5okmkAvj', 'liblzma_la-crc64-fast']

干净的样本不误报:

=== 逻辑验证 2:构造「干净」样本(普通 liblzma 名字) ===
命中: [] (应为空 = 未命中)

但第三个测试踩到了坑,这也是我认为最值得写进正文的一条:

=== 逻辑验证 3:干净样本里出现无关的 "Shutdown" 是否会误报? ===
命中: ['Shutdown'] (说明裸词 Shutdown 会误报)

我构造的样本内容是 systemd-shutdown-helper 和 SystemdShutdown,都不是后门,但裸词 Shutdown 会被子串匹配命中。公开分析里提到可以在 .rodata 段里找这个字符串,而我这一测说明:如果不做边界限定,Shutdown 会把正常的 systemd 相关字符串一起报出来。 想让这条判据可用,至少要加上区分大小写的前缀,或者限定到 .rodata 段并且要求它与 kill switch 变量名同时命中。

第四个测试是版本号解析,xz --version 的输出解析稳定:

=== 逻辑验证 4:版本号提取(xz --version 解析) ===
  'xz (XZ Utils) 5.6.1'   -> 5.6.1
  'xz (XZ Utils) 5.4.6'   -> 5.4.6

第五个测试复现了 Alpine 的判定陷阱,这是我认为检测部分最实用的一条:

=== 逻辑验证 5:Alpine 陷阱复现 ===
 Alpine edge 已修复 5.6.1-r2: 以包修订号判定 -> 5.6.1-r2 => SAFE
 Alpine edge 有后门 5.6.1-r1: 以包修订号判定 -> 5.6.1-r1 => VULNERABLE
 裸 `xz --version` 两边同值 -> 无法区分

也就是说,Alpine edge 上 xz --version 无论 5.6.1-r1(有后门)还是 5.6.1-r2(已修复)都输出 5.6.1,只看上游版本号会得出错误结论。必须查包修订号:

apk list xz
apk list xz | grep xz

判定规则很清楚:修订号带 -r0 或 -r1 的是受影响的,-r2 及以后是修复过的。

排查:三个真实故障与它们各自的修法

下面三个都是官方渠道有记录的真实故障,不是我为了凑数编的「典型报错」。

报错一:迁到 5.6.2 后,MIPS64 上共享库构建直接失败

现象:在 MIPS64 上构建 xz 5.6.2,想出共享库却出不来,而且 --disable-static 静默失效(你以为关掉了静态库,实际没有)。

config.log 里的真实线索(来自项目 issue #142):

configure:9458: checking for file
configure:9479: found /usr/bin/file
configure:9491: result: :

注意这三行:找到了 /usr/bin/file,但结果被赋值成了空。往下走,在 MIPS 分支里判定 ABI 类型的代码依赖 $FILECMD,FILECMD 为空时只得到 emul=elf,于是链接器参数变成 ld -m elf,这个组合根本不工作:

configure:13624: checking whether the gcc linker (/usr/bin/ld -m elf) supports shared libraries
configure:14897: result: no

原因:5.6.2 的发布 tarball 是用 libtool-2.5.0 生成的,而 libtool 2.5.0 当时还是 alpha 版,它有个 bug 导致生成的 configure 不设置 FILECMD。这个问题在多数架构上无害,在 MIPS64 上被放大。

解决方案:项目维护者在 2024-06-19 提供了补丁 xz-5213-547-562-libtool.patch,对应 libtool 上游的修复提交,挂在 5.6.2 的 release 页面下载区,同一个补丁适用于 5.2.13、5.4.7 和 5.6.2 三个版本。issue 里也有人给出了更轻量的绕过办法:在 configure 时显式传 FILECMD=file。

我在这里特别想指出发布说明里那句容易被忽略的话:5.2.13, 5.4.7, and 5.6.2 have a build system issue that prevents building of shared libraries on some systems like mips64。同一个补丁要打三个版本,说明这不是「后门补丁顺手带出来的回归」,而是一次横跨三个分支的构建系统事故。

报错二:Buildroot musl 静态构建报 undefined reference to raise

现象:Buildroot 升到 xz 5.6.2 之后,musl 静态构建失败,链接器报这么一行:

libgcc.a(_dvmd_lnx.o): in function __aeabi_idiv0':
libgcc/config/arm/lib1funcs.S:1499:(.text+0x8): undefined reference to raise

原因:同样来自 FILECMD 缺失。Buildroot 的提交说明写得很直白——如果不跑 autoreconf,LDFLAGS 里的 -static 不会被传下去,于是链接行为不对。提交说明里还写了一句很关键的话:这个故障在 5.4.7 上也能复现,但 5.4.6 不能。也就是说 5.4.7 和 5.6.2 这两个同一天发布的 tarball 有同一个构建缺陷,而 CISA 公告当时建议降级到的正是「5.4.6 或更高版本」——这个建议本身是对的,但如果运维顺手升到了 5.4.7,就换了一个坑。

解决方案:在 Buildroot 的包定义里加上 autoreconf:

XZ_AUTORECONF = YES

对应的上游提交把改动写进了 package/xz/xz.mk,理由写的是 autoreconf 可以修复 musl 静态构建失败。

报错三:Alpine 上 xz --version 显示 5.6.1,但那个包其实已经修好了

现象:在一台 Alpine edge 机器上查版本,输出是 5.6.1,按「上游 5.6.0/5.6.1 受影响」的规则判定为高危,实际上这个包已经修复。

原因:Alpine 的打包版本号带自己的修订后缀,修复版是 5.6.1-r2,但 xz --version 只打印上游版本号,两边都是 5.6.1。

解决方案:不要用 xz --version 做安全判定,用包管理器:

apk list xz

看到 5.6.1-r2 或更高修订号即安全。同类问题在别的发行版也有,Debian 那边甚至把版本号改成了 5.6.1+really5.4.5-1 这种带 +really 的形式——包版本号里出现 +really、-rN 这类后缀,本身就是「上游版本号不足以表达安全状态」的信号,自动化脚本必须去解析完整版本串,只做 startswith("5.6") 一定会误判。

修复本身做了什么,以及为什么 5.6.x 今天已经不会更新了

5.6.2 于 2024-05-29 发布,发布说明第一条就是 Remove the backdoor (CVE-2024-3094)。除此之外有几条与安全态势判断相关的改动值得知道:

关于「今天该用哪个版本」,需要给一个明确的答案。截至本文发稿,xz 的最新版本是 5.8.4(2026-09-09 发布),而官方在发布页已经写明不会再有 5.2.x、5.4.x、5.6.x 的新版本,5.6.x 的最后一个版本停在 2025-01-23 的 5.6.4。这意味着:

今天你实际该做的四件事

按「先确认自己有没有事」到「把这类问题挡在以后」的顺序:

第一步,查包版本而不是查二进制版本。 用发行版自己的包管理器拿到完整版本串,对照上面的表。重点看后缀,+really、-rN 这类都说明安全状态编码在上游版本号之外。

第二步,确认 sshd 是否真的走了 systemd 通知路径。 如果你的 OpenSSH 没有链接到 libsystemd,危害链是断的——Gentoo 和 Wolfi 就是这种情况。用 ldd $(which sshd) | grep liblzma 看有没有间接依赖,这是最快的排除方式。

第三步,如果确实需要手工确认二进制,用带边界限定的特征串。 至少要满足:kill switch 变量名 yolAbejyiejuvnup=Evjtgvsh5okmkAvj 精确匹配,或者 .rodata 段里同时命中多组特征。我上面实测过裸词匹配会误报,别直接抄一条 grep Shutdown 就上线。

第四步,也是这次事故真正的遗产:把「发布产物」和「源码仓库」的差异纳入日常检查。 具体到动作上,构建时固定使用可复现构建、并让 CI 校验构建产物与源码提交的一致性;如果你的流程依赖 GitHub release 的「source code」链接去审计代码,要意识到那条链路线索指向的是仓库内容,不是构建实际吃进去的 tarball。

最后补一句和应急响应有关的:如果确实需要处理一台可能受影响的机器,除了换版本,还要重启 sshd 或整机——降级后旧代码还留在进程内存里,除非重启否则不会消失。我在 事故响应与入侵清理的实战记录 里写过清理顺序的完整版本。

FAQ

Q:CVE-2024-3094 影响 RHEL 和 Ubuntu 吗?

A:都不影响。RHEL 官方公告明确写「No versions of Red Hat Enterprise Linux (RHEL) are affected by this CVE」。Ubuntu 方面,受影响版本只在 noble-proposed 里出现过,在进入 noble 正式分支之前就被撤下了。

Q:怎么判断我的机器有没有被真的攻破?

A:仅凭装了 5.6.0/5.6.1 无法判断。官方立场是这类条件下利用链可能通也可能不通,Red Hat 对 Fedora 40 的表述就是「不含实际恶意利用,但建议降级」。需要查 SSH 认证日志、异常登录模式、非常规子进程和认证耗时变化。CISA 在公告里给的动作是降级 + 排查恶意活动 + 上报发现。

Q:liblzma 被大量程序链接,我是不是所有程序都有风险?

A:不能这样推断。危害链的起点是 sshd 走 systemd 通知,从而间接加载 liblzma。纯应用层使用 liblzma(解压 xz 文件的正常业务)不在这条链上。

Q:为什么删掉那两个测试文件就能修好?

A:Gentoo 的社区里流传过一个改 ebuild 的临时方案,src_unpack 里删掉 m4/build-to-host.m4 以及那两个测试夹具文件,社区版主 Sam James 也验证过它对 5.6.1 有效。但这是应急手段,官方路径是升级到修复版本。

Q:这次事故对个人开发者的实际影响有多大?

A:如果你的机器跑的是发行版 stable 分支且没手动装过 xz 5.6.0/5.6.1,影响基本为零。真正有风险的是追滚动发行版(Arch、Fedora Rawhide、Debian sid、Kali、openSUSE Tumbleweed)并且用 systemd 管理 OpenSSH 的开发者和运维。个人机器上更值得做的是别在生产机器上跑 pre-release 仓库。

Q:后门是「被入侵了」还是「维护者自己干的」?

A:Freund 在原文里的表述是:考虑到活动持续了数周,「the committer is either directly involved or there was some quite severe compromise of their system」,同时他明确认为后者可能性更小——理由是该维护者一直在各个邮件列表上主动讨论那些「修复」提交。本文不对行为主体做进一步判断,只引用这一原始表述。

Q:现在(2026 年)还需要关心 CVE-2024-3094 吗?

A:需要关心的方式变了。业务系统上,主流发行版早就不在受影响版本上了。真正还在流传的风险是老镜像与老构建缓存——2024 年前后构建的容器镜像、CI 缓存、离线安装包里可能仍带着那个版本的 liblzma,而这些东西往往没有跟踪安全更新的渠道。所以现在的排查重点从「我的系统有没有装」转向「我的镜像和构建产物里有没有」。另外记得 5.2.x / 5.4.x / 5.6.x 都已停止更新,需要长期支持应该规划迁到 5.8.x。

👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务

👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速

📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测

🔗 精选推荐工具

使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):

☁️ DigitalOcean 云服务器 ⚡ Vultr 高性能 VPS 🤖 QoderWork 中国版(推荐有奖) ☁️ 阿里云爆款 AI 产品 📚 WordPress 实用书单 🔍 WordPress SEO 书单 🌐 虚拟主机书单 🐳 Docker 书单 🐧 Linux 书单 🐍 Python 书单 💰 联盟营销书单 💵 被动收入书单 🖥️ 服务器书单 ☁️ 云计算书单 🚀 DevOps 书单 🤖 小米 MiMo 开放平台
← 返回首页