Подтвердить что ты не робот

Символы перегрузки текущего процесса (вложение LD_PRELOAD)

Я работаю над профилировщиком кучи для Linux, называемым heaptrack. В настоящее время я полагаюсь на LD_PRELOAD для перегрузки различных (де-) функций распределения, и это работает очень хорошо.

Теперь я хотел бы расширить инструмент, чтобы среда выполнения была прикреплена к существующему процессу, который был запущен без LD_PRELOAD моего инструмента. Я могу dlopen мою библиотеку через GDB просто отлично, но это не будет перезаписывать malloc и т.д. Я думаю, это потому, что в этот момент компоновщик уже разрешил зависимый от позиции код уже запущенного процесса - правильно?

Итак, что мне делать вместо того, чтобы перегружать malloc и друзей?

Я не владею кодом ассемблера. Из того, что я читал до сих пор, я думаю, мне как-то придется заплатить malloc и другие функции, чтобы они сначала перешли к моей функции трассировки, а затем продолжили их фактическую реализацию? Это верно? Как это сделать?

Я надеюсь, что там есть существующие инструменты или я могу использовать GDB/ptrace для этого.

4b9b3361

Ответ 1

Просто для lulz, другое решение, не пытаясь выполнить собственный процесс или не касаясь одной строки сборки или игры с /proc. Вам нужно только загрузить библиотеку в контексте процесса и позволить магии произойти.

Решение, которое я предлагаю, заключается в использовании функции конструктора (приведенной с С++ на C по gcc) для запуска некоторого кода при загрузке библиотеки. Затем эта библиотека просто исправляет запись GOT (Global Offset Table) для malloc. GOT хранит реальные адреса для функций библиотеки, так что разрешение имен происходит только один раз. Для исправления GOT вам нужно поиграть с структурами ELF (см. man 5 elf). И Linux достаточно любезен, чтобы дать вам вектор aux (см. man 3 getauxval), в котором указывается, где найти в памяти заголовки программ текущей программы. Однако лучший интерфейс обеспечивается dl_iterate_phdr, который используется ниже.

Вот пример кода библиотеки, который выполняет именно это, когда вызывается функция init. Хотя то же самое можно было бы достичь с помощью gdb script.

#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.

Если функция конструктора недоступна для вас, все, что вам нужно сделать, это разрешить символ init из недавно загруженной библиотеки и вызвать его.

Обратите внимание, что вы также можете заменить dlopen так, чтобы библиотеки, загруженные после вашей загрузки, также исправлялись. Что может случиться, если вы загрузите свою библиотеку довольно рано или если приложение имеет динамически загружаемые плагины.

Ответ 2

Это не может быть сделано без некоторой настройки с ассемблером. В основном вам придется делать то, что делают gdb и ltrace: найти malloc и виртуальные адреса друзей в образе процесса и поставить точки останова при их входе. Этот процесс обычно включает временное переписывание исполняемого кода, так как вам нужно заменить обычные инструкции на "ловушки" (например, int 3 на x86).

Если вы хотите избежать этого самостоятельно, существует связанная оболочка вокруг gdb (libgdb), или вы можете построить ltrace как библиотеку (libltrace). Поскольку ltrace намного меньше, и разнообразие его библиотеки доступно из коробки, оно, вероятно, позволит вам делать то, что вы хотите при меньших усилиях.

Например, здесь самая лучшая часть файла "main.c" из пакета ltrace:

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