xz-utils后门漏洞 CVE-2024-3094 分析
2024-4-3 01:10:47 Author: govuln.com(查看原文) 阅读量:123 收藏

1

事件背景

其中liblzma库会通过IFUNC劫持sshd,添加后门以实现远程代码执行。该漏洞CVSS评分10分,受到影响的liblzma版本包括:5.6.0和5.6.1。

2

后门漏洞样本分析

本文对该后门漏洞进行了动静态分析,分享了我们整个漏洞的复现和分析的过程,有以下新进展:

1.定位了两处RCE点(调用system())。

2.恢复了部分关键对象,可以确定后门程序调用了哪些系统库函数。

样本获取  

目前xz-utils项目仓库已经无法访问,你可以通过以下方式获取样本:

1. Andres Freund在邮件列表中提供了二进制文件样本liblzma_la-crc64-fast.o.gz;

2. 从Debian仓库[2]下载xz源代码;    

样本分析  

此次攻击流程如下:

1.在编译阶段插入恶意代码;

2.执行阶段构造后门,通过IFUNC劫持sshd进程;

3.触发后门阶段,在特定条件下,通过system实现RCE;

1. 编译阶段注入恶意代码   

攻击者将看似测试的混淆代码推送到数据压缩库liblzma的构建过程中。

在XZ构建过程中,会调用关键脚本build-to-host.m4。在其中会嵌入一个混淆的脚本,与配置的脚本在一起执行。这个注入的脚本在模块xz-utils和liblzma生成MakeFiles过程中发挥着重要作用[3]

另外,liblzma的符号会在许多其他库之前得到解析,包括sshd二进制文件中的符号。在解析符号后,由于编译标志的GOT将被重新映射为只读页面权限-Wl,-z,relro。所以,为了使得恶意代码能够解析尚未加载的库中的符号,后门会在动态链接器中安装挂钩。这个钩子函数从_dl_audit_symbind中被调用,后门将[email protected]的值更改为指向其自己的代码。

编译分为三个阶段[4]

(1)通过M4编译宏,还原恢复恶意数据:

m4/build-to-host.m4文件里有一些相关的代码片段:    

从tests/files/bad-3-corrupt_lzma2.xz文件中读取的字节被输出到标准输入输出。在读取完所有内容后,还会添加一个新行(\n)。

第二步是运行tr(转换,就像是“将字符映射到其他字符”,或者“将字符替换为目标字符”),基本上是将选定的字符(或字节值)更改为其他字符(其他字节值)。于是,我们有tr "\t -" " \t-", 它在从tests/files/bad-3-corrupt_lzma2.xz文件流式传输的字节中进行以下替换:

这实际上是“修复”了bad-3-corrupt_lzma2.xz,使其再次形成一个正确的xz流。在这个阶段的最后一步中,修复后的xz字节流被提取。

(2)通过Bash脚本,提取和合并恶意数据:

该过程包括从一个压缩文件读取字节,通过一系列操作(如字符转换和筛选)来修正和提取数据。    

第二阶段的提取代码,这包括一个复杂的导出语句export i=...。这个命令基本上定义了一个函数,它会在后面的步骤中被调用。

第二阶段提取的第一步是解压缩一个压缩文件,然后这个解压的输出会作为下一步的输入。接下来,执行一个由多个步骤组成的函数i,通过eval $i调用。这个函数主要是一系列的命令,用来处理数据流,要么提取(输出)数据的特定部分,要么跳过(忽略)数据的特定部分。在数据处理的开始,我们会跳过一些数据,比如忽略文件的前1024字节。这个过程反复进行,交替跳过和提取数据直到文件结束。如果文件的开始部分是由重复的字符构成,之后是一些二进制数据。我们通过这种方式去除空白区域,仅合并包含实际数据的区域。    

简而言之,这个过程涉及解压缩数据,逐步处理和提取重要部分,最终得到准备好的数据以进行下一步的操作。

(3)修改编译过程,插入恶意代码:

以上脚本,通过在项目的测试文件夹中搜索包含特定签名的文件,脚本可以定位并执行隐藏于这些文件中的额外脚本或代码。这允许攻击者在不直接修改原始有效payload携带文件的情况下,增加新的恶意功能。    

通过一系列精心设计的命令,在一个特定的.o文件(liblzma_la-crc64-fast.o)中嵌入恶意代码。

2. 执行阶段构造后门  

liblzma_la_crc64_fast.o中get_cpuid()非常可疑。正常情况下,该函数会内联到代码中,GCC源码如下:

 

而后门程序实现如下:

 

看上去sub_A750()应该是_get_cpuid_max(),但实际上它的逻辑不止如此:    

 

当dword_CB60为1时,会调用Llzma_block_param_encoder_0(),该函数是攻击的入口。这里使用了IFUNC技术,liblzma库在加载时,加载器会调用resolver函数,crc32_resolve()和crc64_resolve()这两个函数均会调用get_cpuid()。crc32_resolve()调用时,dword_CB60由0变为1。crc64_resolve()调用时,由于dword_CB60已经为1,后门程序会执行Llzma_block_param_encoder_0()。该函数最终会调用sub_3A10(),后者会检查当前进程是否为sshd,如果是sshd且环境变量符合要求,才会进行攻击:

   

Lsimple_coder_update_0()的功能是通过一个状态自动机来识别字符串,如果是已知字符串,返回该字符串的ID,如果没有则返回0。sub_3A10()先将当前进程的argv[0](/usr/sbin/sshd)传入,然后将结果与0x108进行比较。如果不是0x108,则当前进程不是sshd,后门程序直接返回。

对Lsimple_coder_update_0()下断点可以看到,rdi第一个参数为“/usr/sbin/sshd”的情况。

返回值为0x108时,sub_3A10()会继续调用Lsimple_coder_update_0()进行环境变量的检查:

可以看到此处对env指针列表进行遍历,将每个环境变量字符串传入进Lsimple_coder_update_0(),并检查其返回值,如果返回值不为0则校验不通过,直到env指针列表结束:    

后门程序需要LANG环境变量,如果存在LD_DEBUG变量或者LD_PROFILE变量,后门程序直接返回。满足要求时,后门程序会劫持sshd进程的RSA_public_decrypt()的GOT表:

3. 触发后门  

前面发现Lsimple_coder_update_0()会根据字符串返回对应的ID,通过对该函数下断点,发现system字符串对应的ID为0x9f8。通过搜索0x9f8,发现lzma_alloc函数,该函数能够通过ID找到库函数的地址:

实际上libc_ctx对象保存了libc库相关的函数地址, system函数地址保存在该对象的0x30偏移处:    

libc_ctx位于global_ctx全局对象中,后者保存了多个库的ctx对象,例如libcrypto等:

最后我们根据system_ptr的引用,找到两处RCE点,一处是在RSA_public_decrypt()的HOOK函数调用过程中:

另外一处如下:    

该函数do_RCE()在以下地方被引用:

从而确认该后门漏洞存在RCE。

3

后续工作

如何构造出合法的公钥触发system函数的执行?目前看来该公钥难以构造,后续可能可以对公钥校验的逻辑进行patch,从而对载荷的逻辑进行分析。    

(补充说明:复现环境的liblzma.so.5.4.1为带有后门漏洞的自行编译的so,原本的版本是5.6.1,为了方便调试更改名称为liblzma.so.5.4.1)

4

参考文献

[1].https://openwall.com/lists/oss-security/2024/03/29/4

[2].https://salsa.debian.org/debian/xz-utils/-/tree/debian/unstable?ref_type=heads

[3].https://gynvael.coldwind.pl/?lang=en&id=782

[4].https://www.armosec.io/blog/cve-2024-3094-kubernetes/

[5].https://github.com/karcherm/xz-malware

[6].https://gist.github.com/smx-smx/a6112d54777845d389bd7126d6e9f504    


文章来源: https://govuln.com/news/url/R0K9
如有侵权请联系:admin#unsafe.sh