多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

admin 2024年4月22日03:49:52评论5 views字数 5574阅读18分34秒阅读模式

前言

近日开源软件和供应链安全圈被一个叫做xz/liblzma中存在后门的漏洞CVE-2024-3094给刷屏了,这个漏洞引起诸多研究者和吃瓜群众的关注,甚至出现了多个帖子在“实时文字同步直播分析进展”的“盛大景象”。在如此短的时间内,有如此空前的热度,其中必有非同寻常之处。毕竟从行业现状来看,投毒事件多的都快看麻了,pip、npm、maven甚至github仓库等一年发生那么多投毒事件,为什么这次事件引起如此广泛的关注?

因此,华为熠石联盟进行了专项调查,并将背后的故事一一展开。

事出之巧

Part.01

●●●

微软的postgres开发者Andres Freund在自己的工作中需要去做一些系统基准性能测试,需要系统尽可能干净稳定一点,避免其他进程发出噪音干扰自己的测量。但是巧了,他发现他的sshd进程在疯狂消耗cpu干扰了自己的测试,于是就去调查自己的sshd为什么会出现这个问题,定位之后发现是liblzma部分消耗的。然后同时他又想起了前几天在测试postgres的时候,自打更新了xz包之后valgrind出现的奇奇怪怪的错误,因此对liblzma进行了深入的调查。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

>>>>>

不查不要紧,一查结果发现liblzma的根项目xz项目中被插入了非常隐蔽的恶意代码,而且还是官方的maintainer提交的,这个就很奇怪了,要知道xz压缩工具在各种系统里面可是很常用的,项目的maintainer在其中插入恶意代码这个行为是很严重的,因此他将这个问题也报告给了开源安全社区。对于这个离奇的事故,引起了广泛的关注。

背后的故事:

开源生态现状与隐忧

Part.02

●●●

在熠石联盟调查这个供应链攻击投毒事件后,发现了此次事件背后开源社区甚至开源平台中存在的种种现实乱象。

潜伏两年上位管理者

首先,我们思考一下如何成为一个开源项目/社区的maintainer?这类总结性的问题问问GPT最合适了:

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

定期贡献、与社区互动、展现领导力和主动性、建立信任和声誉、表达对维护者角色的兴趣,展示能力后过渡为维护者,这些总结于情于理都是非常正确的,并且开源社区也的确是在这么个规则下运行的。

因此,回顾我们的主角Jia Tan这两年半的历程,一步一个脚印地按照这个思路和步骤,首先在xz项目中大量参与社区工作,贡献自己的力量,后面通过多种类型的代码贡献,包括修复安全问题、功能bug改进等来证明自己的能力,随后正式成为了xz项目的maintainer。成为maintainer之后继续“无私奉献”了一年多的时间,直到近期才开始悄悄植入恶意代码。

通常,社区对初次提交的代码会格外看一下前因后果,但随着提交者对社区贡献越来越多,参与更多的社区工作,自然会被社区所熟悉和信任,上位管理者也是无可厚非的。这种卧薪尝胆的卧底行为,社区确实难以识别和避免

开源代码有充分审计么

尽管大多数开源项目都会采取审查和审核措施,但仍然可能发生社区在没有进行充分分析和审计的情况下接受代码的情况。尤其是一些小型或个人维护的开源项目,可能缺乏足够的时间、能力资源来进行全面的代码审查和审核,同时如果贡献者在社区中享有良好的声誉或被认为是专家,可能会出现对其提交的代码进行较少审核的情况。在种种情况下,错误地认为提交的代码是合理或安全的情况也确实难以避免。

git仓把代码开源项目就安全么

按照一般的理解,一款开源软件在其git仓公开了源代码后,因为代码的公开可见,经得起大众的审视,所以我们会认为其安全性一般可以信赖。而此次投毒事件相比于此前pip,npm之类的投毒方式,这个作者的投毒方式可谓有点意思。

该事件中的maintainer并没有在git仓中植入恶意代码,而是在手动上传的release包中添加了恶意代码。因此在git仓里面根本看不到他提交恶意代码!

那问题来了,既然代码仓是干净的,那下游直接使用仓库的代码而不用maintainer手动在release中打包的代码不就好了么?

看起来好像是可以的,但Github的接下来的操作可能会给你的这种选择带来麻烦。

开源平台策略

Github作为头部开源平台社区,其提供了便捷的release模块来服务开源软件的版本发布。发布的版本,Github会分为两类文件,一类是maintainer自己上传的文件,如图标识1;另一类是Github根据git仓库自动打包的源码文件,如图标识2。

这两者的区别也可以通过url辨别:

https://github.com/xxxx/v4_loader/releases/download/v4/v4r-release.txt

https://github.com/xxxx/v4_loader/archive/refs/tags/v4.zip

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

这功能也好理解,有些开源项目提供编译好的二进制文件(类型1),开发者可以选择自己上传编译好的二进制文件。

但对于源码交付的开源项目,我们应该怎么选择呢?

是选择类型1仓库管理员上传的源码包文件,还是选择git仓自动生成的类型2的源码包文件呢?

从安全和便捷角度考虑,我们应该选择类型2。毕竟这样可以确保下载得到的代码和Github公开仓库完全一致!当然,安全角度确实如此。但现实中,开源项目真的都会选择用这种方式去获取开源依赖代码么?事实可能并非如此。

选择通过第二类文件去获得源码更安全,获得了代码包之后先校验一下文件Hash也是基本操作,

然后你就会离谱地发现在某些情况下获得到的代码包的Hash可能会不一样!

难道是因为流量被劫持了么?不,这可能与网络问题无关,而是开源平台自身提供的文件Hash变了。

GitHub 上的源代码下载依赖于 Git 的 archive 命令。由于 GitHub 上的数据量很大,Github为了节省存储成本,并不会永久保留类型2的生成的文件。2023年1 月 30 日,Github将 Git 2.38 部署到支持源代码下载的服务中。此版本的 Git 将用于生成类型2压缩包的压缩设置的发生了一点点变更,导致存档文件本身的字节布局发生了变化,最终导致文件Hash变化。

如下图为例:文件2是Github用archive命令生成的源码包,文件1是我手动将文件2(源码包)重新上传到Github的release页面的。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

在1月20日的时候下载这两个文件,将会发现这两个文件的hash是一致的。而在Github发生变更后的1月31日,重新下载这两个文件,将会发现这两个文件的hash不一样了!文件2的hash发生了变化。

对于构建系统,将会发生因为Github的变更导致hash校验不通过无法顺利构建的错误。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

这事引起了严重的后果(GitHub 存档哈希稳定性 ·社区 ·讨论 #46034),以至于Github官方在2023年2月进行了专项回复(Update on the future stability of source code archives and hashes - The GitHub Blog)并回退了这一变更。但同时,Github也明确表示未来将不会保证类型2的文件Hash不变,强迫开源软件社区接受可能会变的事实。

因此,开源领域对Github项目中的代码包分发机制存在一些尴尬的现实:

1、第二类基于git仓库特定tag做的代码包打包,Github并不保证文件hash不变。这个情况的存在,可能会导致在不同时间节点获得到release tar包的hash不一样。如果下载包后用户进行了强hash校验,可能会发生hash不一致导致用户无法判断文件是被谁篡改了的情况。

2、既然类型2的文件Github并不保证hash不变,那社区只能是将类型2的文件下载下来,获得hash,再手动上传成为类型1的文件,就可以避免掉这个问题。

因此,开源社区广泛存在使用类型1文件来获得程序源码的情况,就是因为上Github的机制“逼迫”项目开发者只能打一个源码包二进制文件去保持一个固定的值。

但如果项目的管理者上传的打包文件并不是单纯的项目git仓代码,而是偷偷加了一点料的打包文件,只是通过一样的文件名使其看起来像是源码原封不动的打包,就会发生下游用户可能无法被识别安全风险的情况。

而此次的问题,就直接源自于上面提及的尴尬情况二,即作者在github中上传的代码包中加了一些私料“恶意代码”。而开源社区广泛采用这种方式去发布源码包,一定程度上也可能与平台的策略有关。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

技术分析

Part.03

●●●

后门植入过程

>>

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

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

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

恶意构建过程从m4/build-to-host.m4脚本开始。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

脚本通过字符串替换恢复bad-3-corrupt_lzma2.xz为格式合法的文件,然后使用xz进行解压。替换规则为:

t 替换为 0x20

0x20替换为0x09

-替换为0x5f

_替换为0x2d

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

bad-3-corrupt_lzma2.xz解压之后如下。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

这段代码的目的是从good-large_compressed.lzma中利用head命令提取其中的部分内容,然后使用tr进行字符替换,最后使用xz进行解压,解压之后的结果输出给sh执行。

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

解压之后的代码进行了很多检查,检查之后的一段代码行为十分可疑,它把good-large_compressed.lzma进行一系列的字符替换、解密、xz解压之后写入到了liblzma_la-crc64_fast.o中,这个文件就是最终的后门文件。然后用这个后门文件替换了源码编译的liblzma_la-crc64-fast.o文件。这两个文件名仅有一个下划线和减号的区别,可以看到开发者十分的谨慎。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

后门二进制代码分析

>>

笔者从debian仓库下载xz源码编译,并且从github镜像仓库下载编译之后。使用nm查看liblzma_la-crc64_fast.o的符号信息。上面的是正常的.o文件的符号,仅有几个函数,下面的是后门.o文件,可以包含了很多的符号。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

从文件大小上来后门里面的代码量也很多。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

xz的后门是在lzma_crc64的IFUNC函数中实现的。IFUNC函数类似于用户自定义的解析函数。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

对比源码和后门对象文件。源码中仅调用__get_cpuid来判断,然后根据不同的cpu特征返回不同的处理函数。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析
多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

但是在后门文件中,还调用了sub_4C90这个函数。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

sub_4090函数第一次运行的时候直接调用cpuid,然后返回,但是第二次的时候就会调用sub_4D04函数。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

sub_4D04函数通过传递的a2参数(其实是最开始的栈上的某个地址),然后通过计算得到旧的rbp的地址。然后通过一堆无意义的offset加减得到v5的值,即0x3cff0。然后把0x3cff0赋值给v5。可以看到v5是cpuid在GOT表中的地址。后面的给v5指针指向的地址赋的值通过计算为0x223f0,这里是真正的后门执行函数。上面说的这一堆操作目的就是为了把cpuid的GOT表改成0x223f0函数,劫持cpuid的流程到恶意代码。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

深入分析0x223f0函数,可以看到恶意代码的作者还放了很多看似正常lzma逻辑代码调用,用来迷惑开发者和逆向分析员。后面的主要逻辑在sub_22f50中,代码非常长,而且很乱。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

下面我们直接看被劫持的函数的逻辑。根据openwall的分析,恶意代码更改了sshd的RSA_public_decrypt的GOT表地址,笔者直接通过gdb动态调试查看改之后的地址。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

查看对应的函数,发现调用了sub_17390函数,恶意代码主要在这个函数里面。其中第二个参数3D020是比较重要的一个全局指针变量,恶意代码的所需要的libclibcrypt的函数指针,以及一些全局的数据都存放在这个全局指针里面。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

sub_17390函数首先从RSA公钥中取n,n其实就是payload。然后进行一系列的校验,包含长度等等。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

然后对n进行解密。这里的n前16个字节为iv,后面的为加密数据。这里的iv其实还作为opcode,后面使用。计算方式为opcode = ((QWORD*)iv)[1] + ((DWORD*)iv)[0]  ((DWORD)iv)[0]。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

其中的解密的密钥是动态计算出来的,其实是固定的。将全为0的key和全为0的iv作为参数,使用chacha20解密,解密后的密文作为临时密钥K1。使用K1再次对a2->field_108解密,解密之后获得的密钥K2。之后使用K2对真正的payload进行解密。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

a2->field_108的值如下。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

接着对消息进行签名校验,密钥依然使用之前的动态计算方式。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

check_sig函数里面首先会把后门里面的一个rsa公钥进行sha256计算,返回添加到解密之后的真正的payload后面。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析
多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

然后使用ED448签名算法进行签名校验,这里的公钥和前面动态计算出来的对称密钥K2是一致的。这里我们没有私钥,所以不能伪造合法的签名。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

从这里可以看出消息结构大概如下。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

后面一堆用来混淆的代码,检查opcode,field_0,field_1。但是很多if条件不可能满足。例如opcode必须等于2,否则函数一开始就直接返回了。恶意代码最后的核心目的如下,首先调用setresgid, setresuid,然后调用system执行任意代码,其中的参数就来自于消息中的body字段。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

至此,攻击者利用此后门执行任意命令达到RCE的效果。

临时规避

>>

目前社区尚无最新修复版本,部分主流发行版通过降级5.4.x版本的形式临时规避此问题,请关注官方新版本发布并及时更新。

多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

-END-

原文始发于微信公众号(华为安全应急响应中心):多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月22日03:49:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   多年社区潜伏,一朝功亏一篑 | xz事件思考与技术分析https://cn-sec.com/archives/2628975.html

发表评论

匿名网友 填写信息