创建: 2024-03-29 10:00
修改: 2024-04-08 15:40
https://scz.617.cn/unix/202403291000.txt目录:
☆ 背景介绍
☆ 测试代码
1) some.c
2) ifunctest.c
☆ 整体流程框架
☆ liblzma后门
2) 第一步Hook
3) IFUNC机制的作用
☆ 参考资源
☆ 背景介绍
参看
https://scz.617.cn/unix/202403290900.txt
某些版本liblzma.so被植入后门代码,被全世界的安全人员鞭尸式分析,目前已知其包含但不限于如下功能:
Command 0x00 Unknown
Command 0x01 SSH authentication bypass
Command 0x02 Execute shell command
Command 0x03 Execute shell command with specified UID/GID
即是说,不只是远程代码执行,也确有登录认证绕过。其后门协议涉及Ed448椭圆曲线签名算法,相应私钥只为作恶方所掌握。故,即便有暴露在公网的后门,除了作恶者,其他人无法利用该后门。已知相关PoC均需Patch恶意liblzma.so中Ed448公钥,仅有研究意义。
安全人员对后门功能研究得越来越深入、细致,围观即可。相比之下,我对后门第一步Hook如何完成更好奇些,后来ZYH、Lenny Wang分别回答了这个问题。
本文从正常程序员角度初探IFUNC机制,与liblzma后门并非强相关,但也不是无关。
☆ 测试代码
1) some.c
/*
* gcc -fPIC -shared -Wl,-soname,libsome.so -Wl,-m,elf_x86_64 -Wall -pipe -O0 -g3 -o libsome.so some.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>static void foo_0 ( void )
{
printf( "call foo_0()\n" );
}
__attribute__((used))
static void foo_1 ( void )
{
printf( "call foo_1()\n" );
}
__attribute__((used))
static void * foo_resolver ( void )
{
printf( "call foo_resolver()\n" );
return ( void * )&foo_0;
}
extern void foo( void ) __attribute__((ifunc("foo_resolver")));
__attribute__((constructor))
static void some_init ( int argc, char **argv, char **envp )
{
printf( "call some_init()\n" );
}
some.c对应动态链接库libsome.so,只有名为foo的导出函数,其符号解析由foo_resolver完成,后者返回哪个函数指针,foo就对应哪个函数,foo_resolver这个符号并未导出。
常规导出函数是静态导出,链接时导出表已确定;IFUNC导出函数是动态导出,运行时由"ifunc resolver"决定导出谁,填写导出表。本例foo_resolver直接返回foo_0,实际中则是基于某种条件决定返回foo_0、foo_1中的某一个,后面会展示liblzma.so的"ifunc resolver"实现。
"ifunc resolver"的调用时机非常早期,其被调用时环境变量尚未就位,getenv()啥也取不到。换句话说,不要指望通过环境变量向"ifunc resolver"传递参数。
some_init与IFUNC机制无关,用于其他测试目的。
2) ifunctest.c
/*
* gcc -Wall -pipe -O0 -g3 -o ifunctest ifunctest.c -L. -lsome
*
* LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./ifunctest
*/
#include <stdio.h>extern void foo ( void );
int main ( int argc, char * argv[] )
{
printf( "call main()\n" );
foo();
foo();
return 0;
}
ifunctest是主程序,会动态链接libsome.so,导入来自后者的foo函数,本例实际导入foo_0。
$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH FOO_RESOLVER=1 ./ifunctest
call foo_resolver()
call some_init()
call main()
call foo_0()
call foo_0()
从printf结果看出,foo_resolver甚至比some_init还要早调用,不只是"before main"、"before _start",更是"before .init_array"。
☆ 整体流程框架
用GDB调试ifunctest,大概有下面这些流程:
一些关键节点的执行顺序:
ld-linux.so e_entry
ifunc resolver
.init_array[]
normal e_entry
normal main
☆ liblzma后门
2) 第一步Hook
From ZYH & Lenny Wang
第一步Hook通过修改源码完成,原来的代码是:
/*
* xz-5.6.0\src\liblzma\check\crc64_fast.c
*/
#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")));static crc64_func_type
crc64_resolve(void)
{
return is_arch_extension_supported()
? &crc64_arch_optimized : &crc64_generic;
}
liblzma.so动态导出符号lzma_crc64,加载时由crc64_resolve根据CPU情况决定该符号对应crc64_arch_optimized、crc64_generic中的某一个。crc64_resolve本身不是导出符号。
改过的代码是:
static crc64_func_type
crc64_resolve(void)
{
/*
* 前面多了个下划线
*/
return _is_arch_extension_supported()
? &crc64_arch_optimized : &crc64_generic;
}
修改源码动作在injected.txt中。用了管道,改过的源码没有落盘,与恶意payload一起生成新的.o
3) IFUNC机制的作用
crc64_resolve是"ifunc resolver",加载liblzma.so时,会自动调用它,无需显式调用。其调用时机非常之早,比liblzma.so可能存在的.init_array[]还要早,在"catch load liblzma"命中之前就被调用了,这是一种超级"before _start"机制。换句话说,只要某个ELF直接、间接依赖liblzma.so,启动该ELF时,crc64_resolve就会得到执行机会。过去反入侵检测时会检查ELF的.init_array[],现在应该增加对"ifunc resolver"的检查,比如:
objdump -CT liblzma.so.5.6.0 | grep "g iD"
nm -CD liblzma.so.5.6.0 | grep " i "
readelf -W --dyn-syms --demangle liblzma.so.5.6.0 | grep -E "IFUNC GLOBAL DEFAULT"
可用IDA反汇编so,查看lzma_crc64,进而定位crc64_resolve。
☆ 参考资源
https://sourceware.org/glibc/wiki/GNU_IFUNCxz/liblzma后门恶意代码注入方式分析 - Lenny Wang, UID(2045181921) [2024-04-03]
https://lennysec.github.io/xz-backdoor-code-injection-analysis/