【问题标题】:Overload symbols of running process (LD_PRELOAD attachment)运行进程的重载符号(LD_PRELOAD 附件)
【发布时间】:2015-01-24 02:16:36
【问题描述】:

我正在开发一个用于 Linux 的堆分析器,名为 heaptrack。目前,我依靠LD_PRELOAD 重载各种(取消)分配函数,效果非常好。

现在我想扩展该工具以允许运行时附加到现有进程,该进程是在没有LD_PRELOADing 我的工具的情况下启动的。我可以通过 GDB dlopen 我的库就好了,但这不会覆盖 malloc 等。我认为,这是因为此时链接器已经解决了已经运行的进程的位置相关代码 - 对吗?

那么我该怎么做才能让malloc 和朋友超载呢?

我不精通汇编代码。从我目前所读到的内容来看,我想我必须以某种方式修补malloc 和其他函数,以便它们首先回调我的跟踪函数,然后继续它们的实际实现?那是对的吗?我该怎么做?

我希望有现成的工具,或者我可以利用 GDB/ptrace 来实现。

【问题讨论】:

  • 我偶然发现了 ltrace,它应该支持运行时附加,但是 malloc 过滤器将无法工作。所以我有一种感觉,简单的 ptrace 方法行不通?
  • @milianw:我相信我已经描述了一个基于 ptrace 的解决方案 here;你知道吗?该答案中的后一个示例将地址替换为 write 系统调用,在您的情况下,您可以将目标函数的初始部分替换为插入函数的跳转。该技术并不简单(困难的部分是在目标二进制文件中找到要覆盖的地址),而且它非常特定于架构,但是在插入之后,根本没有额外的开销或速度损失。
  • @IwillnotexistIdonotexist:我还看到了补丁malloc()memalign()posix_memalign()free() 等。作为要走的路。使用 ptrace 附加到目标进程,并匿名映射可写页面,然后将与位置无关的可执行代码复制到该页面,一点也不难。附加过程可以使用精灵工具和/proc/PID/maps来定位目标地址。这甚至适用于静态二进制文件(无 libdl)。困难的部分是在跳转指令下反汇编/复制 asm op(s)——当然,除非它本身就是跳转指令。
  • @IwillnotexistIdonotexist:我已经探索了 ptracing 多线程进程 in this answer,包括单步执行单个线程;它看起来健壮而直接。在 x86-64 上,序言(被替换的部分)为 5 到 13 个字节——如果替换代码在 %rip 的 32 位偏移范围内,则为 5 个字节,如果需要任意 64 位 pushq %rax; movabs $constant, %rax ; jmp *%rax 序列,则为 13 个字节。指令分析(那 5-13 个字节)是讨厌的。我更喜欢 mmap 完整的替换功能。这是一个可以接受的选择吗?
  • @IwillnotexistIdonotexist:没错!如果代码使用来自已知 C 库版本的函数,那么我们可以知道函数地址范围(通过针对相同库版本编译测试二进制文件);和 glibc 等人。仅与函数本身而不是函数内部有公共链接。为了健壮性,可以单步执行每个线程,直到它完全脱离 C 库代码。但是,这将导致需要针对使用的每个 c 库版本编译帮助代码......另一方面,没有指令分析!

标签: c linux gdb runtime dynamic-linking


【解决方案1】:

如果不稍微调整一下汇编程序就无法做到这一点。基本上,您将不得不做 gdb 和 ltrace 所做的事情:在进程映像中找到 malloc 和朋友的虚拟地址,并在其入口处放置断点。此过程通常涉及临时重写可执行代码,因为您需要将普通指令替换为“陷阱”指令(例如 x86 上的int 3)。

如果您想避免自己这样做,可以使用 gdb (libgdb) 周围的可链接包装器,或者您可以将 ltrace 构建为库 (libltrace)。由于 ltrace 小得多,而且它的库种类是开箱即用的,它可能会让你以更少的努力做你想做的事。

例如,这是 ltrace 包中“main.c”文件的最佳部分:

int
main(int argc, char *argv[]) {
    ltrace_init(argc, argv);

 /*
    ltrace_add_callback(callback_call, EVENT_SYSCALL);
    ltrace_add_callback(callback_ret, EVENT_SYSRET);
    ltrace_add_callback(endcallback, EVENT_EXIT);

    But you would probably need EVENT_LIBCALL and EVENT_LIBRET
 */

    ltrace_main();
    return 0;
}

http://anonscm.debian.org/cgit/collab-maint/ltrace.git/tree/?id=0.7.3

【讨论】:

  • 感谢您的提示。不过,LTrace 的开销似乎非常高。如此之高,以至于我使用它变得不切实际。我可能需要等待 perf 子系统支持本机“脚本”,然后我可以使用它来连接到自定义用户空间断点...
  • 你最终会在同一个地方。任何类型的执行跟踪都相当慢,甚至硬件断点也会大大减慢速度。在我看来,唯一合理快速的方法是扫描为进程加载的所有模块,然后使用它们的磁盘映像作为参考,为感兴趣的符号重做动态链接过程(因此不是链接到 malloc,进程映像将现在链接到会计存根,转发到 malloc)。这本身并不难,但要做到这一点可能需要付出相当大的努力。
  • 所以 ltrace 或类似的 GDB 不能只为我重写一次然后“分离”吗?我的意思是在 libc 中重写的 malloc/free 之后,除了额外的跳转和我在自己的工具中添加的内容之外,我希望不会有更多的开销。为什么不是这样?
  • “热” dll 注入问题主要是开发漏洞利用的人感兴趣的,所以这些东西不是很公开。这是一个“热”符号注入器的示例:github.com/ice799/injectso64
  • 如果我错了,请纠正我,但不是injectso64“只是”注入一个共享库吗?这也可以通过手动调用 dlopen 使用一个小的 GDB 脚本来(很简单地)完成。还是injectso64 也重写了函数?这才是我真正感兴趣的。也许 LTTng 就是我要找的东西?
【解决方案2】:

只是为了 lulz,另一种解决方案,无需跟踪您自己的流程,也无需触及一条装配线,也无需使用 /proc。您只需在进程的上下文中加载库并让奇迹发生。

我建议的解决方案是使用 constructor 功能(由 gcc 从 C++ 引入 C)在加载库时运行一些代码。然后这个库只是修补了malloc 的 GOT(全局偏移表)条目。 GOT 存储库函数的真实地址,因此名称解析只发生一次。要修补 GOT,您必须使用 ELF 结构(请参阅man 5 elf)。 Linux 很友好地为您提供了aux 向量(请参阅man 3 getauxval),它告诉您在内存中的哪里可以找到当前程序的程序头。不过更好的接口是dl_iterate_phdr提供的,下面使用。

这是一个库的示例代码,它在调用init 函数时正是这样做的。尽管使用 gdb 脚本也可以实现同样的效果。

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <sys/auxv.h>
#include <elf.h>
#include <link.h>
#include <sys/mman.h>


struct strtab {
    char *tab;
    ElfW(Xword) size;
};


struct jmpreltab {
    ElfW(Rela) *tab;
    ElfW(Xword) size;
};


struct symtab {
    ElfW(Sym) *tab;
    ElfW(Xword) entsz;
};



/* Backup of the real malloc function */
static void *(*realmalloc)(size_t) = NULL;


/* My local versions of the malloc functions */
static void *mymalloc(size_t size);


/*************/
/* ELF stuff */
/*************/
static const ElfW(Phdr) *get_phdr_dynamic(const ElfW(Phdr) *phdr,
        uint16_t phnum, uint16_t phentsize) {
    int i;

    for (i = 0; i < phnum; i++) {
        if (phdr->p_type == PT_DYNAMIC)
            return phdr;
        phdr = (ElfW(Phdr) *)((char *)phdr + phentsize);
    }

    return NULL;
}



static const ElfW(Dyn) *get_dynentry(ElfW(Addr) base, const ElfW(Phdr) *pdyn,
        uint32_t type) {
    ElfW(Dyn) *dyn;

    for (dyn = (ElfW(Dyn) *)(base + pdyn->p_vaddr); dyn->d_tag; dyn++) {
        if (dyn->d_tag == type)
            return dyn;
    }

    return NULL;
}



static struct jmpreltab get_jmprel(ElfW(Addr) base, const ElfW(Phdr) *pdyn) {
    struct jmpreltab table;
    const ElfW(Dyn) *dyn;

    dyn = get_dynentry(base, pdyn, DT_JMPREL);
    table.tab = (dyn == NULL) ? NULL : (ElfW(Rela) *)dyn->d_un.d_ptr;

    dyn = get_dynentry(base, pdyn, DT_PLTRELSZ);
    table.size = (dyn == NULL) ? 0 : dyn->d_un.d_val;
    return table;
}



static struct symtab get_symtab(ElfW(Addr) base, const ElfW(Phdr) *pdyn) {
    struct symtab table;
    const ElfW(Dyn) *dyn;

    dyn = get_dynentry(base, pdyn, DT_SYMTAB);
    table.tab = (dyn == NULL) ? NULL : (ElfW(Sym) *)dyn->d_un.d_ptr;
    dyn = get_dynentry(base, pdyn, DT_SYMENT);
    table.entsz = (dyn == NULL) ? 0 : dyn->d_un.d_val;
    return table;
}



static struct strtab get_strtab(ElfW(Addr) base, const ElfW(Phdr) *pdyn) {
    struct strtab table;
    const ElfW(Dyn) *dyn;

    dyn = get_dynentry(base, pdyn, DT_STRTAB);
    table.tab = (dyn == NULL) ? NULL : (char *)dyn->d_un.d_ptr;
    dyn = get_dynentry(base, pdyn, DT_STRSZ);
    table.size = (dyn == NULL) ? 0 : dyn->d_un.d_val;
    return table;
}



static void *get_got_entry(ElfW(Addr) base, struct jmpreltab jmprel,
        struct symtab symtab, struct strtab strtab, const char *symname) {

    ElfW(Rela) *rela;
    ElfW(Rela) *relaend;

    relaend = (ElfW(Rela) *)((char *)jmprel.tab + jmprel.size);
    for (rela = jmprel.tab; rela < relaend; rela++) {
        uint32_t relsymidx;
        char *relsymname;
        relsymidx = ELF64_R_SYM(rela->r_info);
        relsymname = strtab.tab + symtab.tab[relsymidx].st_name;

        if (strcmp(symname, relsymname) == 0)
            return (void *)(base + rela->r_offset);
    }

    return NULL;
}



static void patch_got(ElfW(Addr) base, const ElfW(Phdr) *phdr, int16_t phnum,
        int16_t phentsize) {

    const ElfW(Phdr) *dphdr;
    struct jmpreltab jmprel;
    struct symtab symtab;
    struct strtab strtab;
    void *(**mallocgot)(size_t);

    dphdr = get_phdr_dynamic(phdr, phnum, phentsize);
    jmprel = get_jmprel(base, dphdr);
    symtab = get_symtab(base, dphdr);
    strtab = get_strtab(base, dphdr);
    mallocgot = get_got_entry(base, jmprel, symtab, strtab, "malloc");

    /* Replace the pointer with our version. */
    if (mallocgot != NULL) {
        /* Quick & dirty hack for some programs that need it. */
        /* Should check the returned value. */
        void *page = (void *)((intptr_t)mallocgot & ~(0x1000 - 1));
        mprotect(page, 0x1000, PROT_READ | PROT_WRITE);
        *mallocgot = mymalloc;
    }
}



static int callback(struct dl_phdr_info *info, size_t size, void *data) {
    uint16_t phentsize;
    data = data;
    size = size;

    printf("Patching GOT entry of \"%s\"\n", info->dlpi_name);
    phentsize = getauxval(AT_PHENT);
    patch_got(info->dlpi_addr, info->dlpi_phdr, info->dlpi_phnum, phentsize);

    return 0;
}



/*****************/
/* Init function */
/*****************/
__attribute__((constructor)) static void init(void) {
    realmalloc = malloc;
    dl_iterate_phdr(callback, NULL);
}



/*********************************************/
/* Here come the malloc function and sisters */
/*********************************************/
static void *mymalloc(size_t size) {
    printf("hello from my malloc\n");
    return realmalloc(size);
}

还有一个示例程序,它只在两次 malloc 调用之间加载库。

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



void loadmymalloc(void) {
    /* Should check return value. */
    dlopen("./mymalloc.so", RTLD_LAZY);
}



int main(void) {
    void *ptr;

    ptr = malloc(42);
    printf("malloc returned: %p\n", ptr);

    loadmymalloc();

    ptr = malloc(42);
    printf("malloc returned: %p\n", ptr);

    return EXIT_SUCCESS;
}

mprotect 的调用通常是无用的。但是我发现 gvim(编译为共享对象)需要它。如果您还想捕获对malloc 的引用作为指针(这可能允许稍后调用实际函数并绕过您的函数),您可以将相同的过程应用于DT_RELA 动态条目指向的符号表。

如果 constructor 功能对您不可用,您所要做的就是从新加载的库中解析 init 符号并调用它。

请注意,您可能还想替换dlopen,以便在您的库之后加载的库也得到修补。如果您过早加载库或应用程序具有动态加载的插件,则可能会发生这种情况。

【讨论】:

  • 这看起来很有希望。事实上,它适用于您添加的简单测试。但在更复杂的情况下,例如当我通过 gdb 连接到一个更大的应用程序然后 call (void) dlopen("/tmp/libinject.so", 0x0001) 那里时,我看到 lib 被初始化,但找不到 malloc 地址。例如,当我尝试使用 kwrite 时,它找到的符号是 __libc_start_main__gmon_start__kdemain
  • 顺便说一句,如果我有时间,我肯定打算更多地研究你的代码。这看起来非常有希望。如果最终成功,我还能用赏金积分“捐赠”你吗?如果是这样,如果这最终能解决,我会非常愿意。
  • 你有理由使用getauxval而不是dl_iterate_phdr from link.h吗?
  • 我尝试使用dl_iterate_phdr 遍历所有动态部分,希望即使在加载到共享库中但无法正常工作的应用程序中也可以使这种重载工作。 . 我的代码在这里(注意:C++11 语法)paste.kde.org/ptobkcije p_flags 检查可读和可写的动态部分...任何想法我做错了什么?我需要在某个地方打电话给mprotect吗?
  • 哇!我明白了,对于共享库,我需要考虑dlpi_addr 偏移量!两者,当从p_vaddr 转换ElfW(Dyn) 时,以及在编写符号rela-&gt;r_offset 时,我都需要添加dlpi_addr 偏移量,它神奇地起作用了。非常感谢 Celelibi,没有你的帮助,我永远找不到写这篇文章的方法!我该如何向你表达我的感激之情?我现在已经接受了你的回答,但原来的赏金已经超时。我可以再给你一个赏金吗?还是别的什么?非常感谢,真的!这是我最新代码的链接:bit.ly/1Axmk4Y
猜你喜欢
  • 2012-06-26
  • 1970-01-01
  • 1970-01-01
  • 2015-04-25
  • 2019-05-04
  • 1970-01-01
  • 2016-08-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多