xz/liblzma 后门恶意代码注入方式分析
xz 后门爆出来之后,各路分析文章已经很多了,不过还是有些细节没有讲得很清楚,偶然看到四哥提到:
“作为围观群众,我在等待分析方展示一下,Hook 是怎么安装上去的,就是说 .so 被加载后,怎么就 call 到那个 .o 里去了,我问的是第一次。换句话说,是不是与 IFUNC 相关,究竟怎么完成第一步 Hook?”
正好我跟四哥有同样的疑问,而且没有文章讲到这个问题,所以就来简单分析一下这个点,留作笔记。
样本获取
我是从 Fedora Builds 上获取的样本:
https://koji.fedoraproject.org/koji/buildinfo?buildID=2417414
文件对应关系:
1 | xz-libs-5.6.1-1.fc41.x86_64.rpm: liblzma.so.5.6.1 二进制文件 |
rpm 文件提取方法:
1 | rpm2cpio <path_to_rmp_file> | cpio -idmv |
攻击入口
和后门直接相关的 commit 有三处:
2024.02.15: 为 m4/build-to-host.m4 添加了 gitignore 规则
https://git.tukaani.org/?p=xz.git;a=commit;h=4323bc3e0c1e1d2037d5e670a3bf6633e8a3031e
2024.02.23: 增加了一些测试文件
https://git.tukaani.org/?p=xz.git;a=commit;h=cf44e4b7f5dfdbf8c78aef377c10f71e274f63c0
2024.03.09: 更新了两个测试文件:
https://git.tukaani.org/?p=xz.git;a=commit;h=6e636819e8f070330d835fce46289a3ff72a7b89
m4/build-to-host.m4 便是攻击入口,这个脚本从如下这两个测试文件中读取、解压、揭秘恶意代码:
1 | tests/files/bad-3-corrupt_lzma2.xz |
关于恶意代码提取的部分,Gynvael Coldwind 已经给出了漂亮的分析,不再赘述:
xz/liblzma: Bash-stage Obfuscation Explained - Gynvael Coldwind
Andres Freund 的原始报告中也直接给出了还原后的 shell 脚本和攻击 payload .o 文件:
backdoor in upstream xz/liblzma leading to ssh server compromise - Andres Freund
After de-obfuscation this leads to the attached injected.txt.
Florian Weimer first extracted the injected code in isolation, also attached, liblzma_la-crc64-fast.o, I had only looked at the whole binary.
恶意代码注入方式
现在我们有了 payload liblzma_la-crc64-fast.o 文件,但 .o 文件里的 payload 是如何注入到目标程序中的呢?
我们仔细看一下还原后的 shell 脚本在生成了 liblzma_la-crc64-fast.o 之后的内容,对应原报告附件 injected.txt line 178 之后的内容。
我们看关键的几行:
1 | V='#endif\n#if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) || defined(BUILDING_CRC32_CLMUL))\nextern int _get_cpuid(int, void*, void*, void*, void*, void*);\nstatic inline bool _is_arch_extension_supported(void) { int success = 1; uint32_t r[4]; success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; }\n#else\n#define _is_arch_extension_supported is_arch_extension_supported' |
sed 是 stream editor,用来修改文件流的,我们只需要简单知道一些 sed 的基础操作命令含义即可,比如:a(append), c(replace), d(delete), i(insert).
这段脚本的关键作用就是在 crc_x86_clmul.h 头文件中添加变量 V 中的代码,把 crc64_fast.c 中的
1 | return is_arch_extension_supported() |
改成:
1 | return _is_arch_extension_supported() |
is_arch_extension_supported()
函数被 crc64_resolve()
函数调用。原始 crc64_resolve()
函数如下:
1 | lzma_resolver_attributes |
即修改了 crc64_resolve()
函数的内容。
把变量 V 中的代码整理一下:
1 |
|
这样应该就好理解了:先用 #endif
闭合原有的 #if
,然后增加了一个 _is_arch_extension_supported()
函数,其中子函数 _get_cpuid()
是 extern 函数,也就是从 payload liblzma_la-crc64-fast.o 导入。简单说就是用 payload 中的 _is_arch_extension_supported()
函数替换了原来的 is_arch_extension_supported()
函数。
$CC 一行就是调用 gcc 将 payload liblzma_la-crc64-fast.o 编译成一个可重定位目标文件,并将其保存为 .libs 目录下 的 liblzma_la-crc64_fast.o 文件中。后面有针对 crc32_fast 类似的操作,不再赘述。
再通过 link 命令将 .o 链接成 .so 文件:
1 | if $AM_V_CCLD$liblzma_la_LINK -rpath $libdir $liblzma_la_OBJECTS $liblzma_la_LIBADD; then |
再后面就是一些清理工作,不再赘述。
总结
回到最开始四哥的问题:
“作为围观群众,我在等待分析方展示一下,Hook 是怎么安装上去的,就是说 .so 被加载后,怎么就 call 到那个 .o 里去了,我问的是第一次。换句话说,是不是与 IFUNC 相关,究竟怎么完成第一步 Hook?”
从我自己的分析来看,可以得出如下结论:
- .o 文件中的 payload 调用不是动态加载的,而是通过修改源代码和编译过程直接编译进目标 .so 文件中的;
- 第一步恶意代码的注入和 IFUNC 机制没有关系,只是攻击者选择了 IFUNC Resolver 函数作为代码注入的目标而已。
我们再来看 Andres Freund 的原报告中是怎么说的:
The backdoor initially intercepts execution by replacing the ifunc resolvers crc32_resolve(), crc64_resolve() with different code, which calls _get_cpuid(), injected into the code (which previously would just be static inline functions).
嗯,说得很正确,就是 replacing,replacing 的对象是 IFUNC Resolvers. 很多文章中对这个点要么语焉不详,要么以讹传讹、不求甚解,这样不好。