Трассировка процессов с помощью Ptrace -- Часть 3

Автор: Sandeep S
Перевод: Андрей Киселев

В первой части мы с вами рассмотрели основы работы с ptrace. Во второй части был приведен пример небольшой программы, которая изменяла содержимое регистров процессора в другом приложении, заставляя его выполнить внедренный код. На этот раз мы будем учиться обращаться к памяти трассируемого процесса. Цель данной части состоит в том, чтобы продемонстрировать методику доступа к идентификаторам процесса во время исполнения. Область применения этой методики настолько широка, что ограничивается лишь вашей фантазией.


1. Введение.

Мы уже знакомы с ptrace и знаем как начать трассировку существующего процесса, как выполнять и как завершать ее. Мы также познакомились с форматом двоичных исполняемых файлов Linux -- ELF.

Поскольку мы намереваемся подключиться к исполняющемуся процессу и получить доступ к его идентификаторам, нам необходимо установить их местоположение, а для этого нужно обратиться к link_map -- внутренней структуре динамического компоновщика, которая хранит некоторые сведения о загруженных библиотеках и идентификаторах в этих библиотеках.

Объявление структуры link_map (см. /usr/include/link.h) выглядит так:

struct link_map
  {
    ElfW(Addr) l_addr;      /* Базовый адрес загруженного объекта.  */
    char *l_name;           /* Полное имя файла объекта.  */
    ElfW(Dyn) *l_ld;        /* Динамическая секция разделяемого объекта.  */
    struct link_map *l_next, *l_prev; /* Ссылки на загруженные объекты.  */
  };


Краткое описание полей структуры.

Link-map -- это двусвязный список, каждый элемент которого имеет ссылку на загруженную библиотеку. Все что нам нужно -- это пройти по списку и отыскать требуемый идентификатор. Теперь мы подошли к вопросу: "И где же взять этот link_map?"

Для каждого объектного файла создается Глобальная Таблица Смещений (global offset table -- GOT). Второй элемент этой таблицы как раз и отвечает за link_map. Так что нам остается лишь забрать адрес link_map из GOT[1] и найти искомый идентификатор.

2. Пример кода.

Теперь мы владеем основными сведениями, которые нам понадобятся, можно начинать. Прежде всего начнем трассировку процесса 'pid', а затем отыщем link_map. В файле с исходным текстом примера вы найдете ряд вспомогательных функций, таких как read_data, read_str и пр., которые значительно облегчают жизнь программиста при работе с ptrace. Назначение функций очевидно из их названий.

Функция для поиска link_map :

struct link_map *locate_linkmap(int pid)
{
    Elf32_Ehdr *ehdr = malloc(sizeof(Elf32_Ehdr));
    Elf32_Phdr *phdr = malloc(sizeof(Elf32_Phdr));
    Elf32_Dyn *dyn = malloc(sizeof(Elf32_Dyn));
    Elf32_Word got;
    struct link_map *l = malloc(sizeof(struct link_map));
    unsigned long phdr_addr, dyn_addr, map_addr;
    
    read_data(pid, 0x08048000, ehdr, sizeof(Elf32_Ehdr));
    phdr_addr = 0x08048000 + ehdr->e_phoff;
    printf("program header at %p\n", phdr_addr);
    read_data(pid, phdr_addr, phdr, sizeof(Elf32_Phdr));

    while (phdr->p_type != PT_DYNAMIC) {
        read_data(pid, phdr_addr += sizeof(Elf32_Phdr), phdr,
                             sizeof(Elf32_Phdr));
    }
    
    read_data(pid, phdr->p_vaddr, dyn, sizeof(Elf32_Dyn));
    dyn_addr = phdr->p_vaddr;

    while (dyn->d_tag != DT_PLTGOT) {
        read_data(pid, dyn_addr += sizeof(Elf32_Dyn), dyn, sizeof(Elf32_Dyn));
    }

    got = (Elf32_Word) dyn->d_un.d_ptr;
    got += 4;           /* помните? второй элемент таблицы GOT. */

    read_data(pid, (unsigned long) got, &map_addr, 4);
    read_data(pid, map_addr, l, sizeof(struct link_map));
    free(phdr);
    free(ehdr);
    free(dyn);
    return l;
}

Поиск начинается с адреса 0x08048000, где размещен ELF-заголовок трассируемого процесса. По содержимому ELF-заголовка определяется местоположение программного заголовка. (Структура заголовков обсуждалась во второй части статьи.) После того как будет получен программный заголовок, выполняется поиск заголовка с информацией о динамическом связывании. Затем по полученной информации выполняется поиск базового адреса глобальной таблицы смещений.

После того как получен адрес таблицы -- из нее читается второй элемент, где находится адрес link_map, а из полученного адреса читается содержимое link_map, которое и возвращается в качестве результата.

Мы получили в свое распоряжение struct link_map, а теперь надо получить таблицу символов (symtab) и таблицу строк (strtab). Для этого обратимся к полю l_ld структуры link_map и пройдемся по набору динамических секций, пока не обнаружим секции DT_SYMTAB и DT_STRTAB, в этих секциях мы как раз и попытаемся обнаружить искомые идентификаторы.

Функция поиска таблицы символов и таблицы строк:

void resolv_tables(int pid, struct link_map *map)
{
    Elf32_Dyn *dyn = malloc(sizeof(Elf32_Dyn));
    unsigned long addr;
    addr = (unsigned long) map->l_ld;
    read_data(pid, addr, dyn, sizeof(Elf32_Dyn));
    while (dyn->d_tag) {
        switch (dyn->d_tag) {
        case DT_HASH:
            read_data(pid, dyn->d_un.d_ptr + map->l_addr + 4, 
                       &nchains, sizeof(nchains));
            break;
        case DT_STRTAB:
            strtab = dyn->d_un.d_ptr;
            break;
        case DT_SYMTAB:
            symtab = dyn->d_un.d_ptr;
            break;
        default:
            break;
        }
        addr += sizeof(Elf32_Dyn);
        read_data(pid, addr, dyn, sizeof(Elf32_Dyn));
    }
    free(dyn);
}

Эта функция проходит по динамическим секциям, проверяя каждую -- не содержит ли она признак DT_STRTAB или DT_SYMTAB. Если секция имеет один из этих признаков, то адрес таблицы запоминается в соответствующем указателе strtab или symtab. Цикл завершается после того, как будут просмотрены все динамические секции.

Следующий наш шаг -- получить значение идентификатора из таблицы символов. Для этого проверяются все элементы таблицы идентификаторов, не являются ли они именами функций. (Нас интересует возможность обнаружения библиотечной функции). Если данный идентификатор -- имя функции, то затем он сравнивается с заданным именем функции. Если они совпадают, то возвращается значение идентификатора.

Теперь мы получили значение идентификатора, что собственно и требовалось. Как его можно использовать? Ответ на этот вопрос зависит уже от вас, уважаемый читатель. Как и все в этом мире, его можно использовать как во благо так и во вред.

У вас может сложиться впечатление, что на этом мы закончили -- но это не так. Мы забыли выполнить еще один обязательный шаг -- "отпустить" приложение, т.е. закончить трассировку. Если этого не сделать, то трассируемое приложение останется в состоянии останова "на веки вечные", последствия этого мы частично рассматривали в части I. Так что прежде чем завершить работу мы завершаем трассировку.

Пример программы вы найдете в файле Ptrace.c

Соберите программу командой

#cc Ptrace.c -o symtrace

А теперь протестируем ее. Запустите какое либо приложение (в другом терминале) и подайте следующую команду. (Здесь я хочу уточнить, что в качестве "подопытного" приложения я запускал emacs и искал идентификатор strcpy). Вы можете выбрать для экспериментов любое другое приложение и попытаться отыскать в нем любой другой идентификатор.

#./symtrace `ps ax | grep 'emacs' | cut -f 2 -d " "` strcpy

От переводчика: на моей системе этот вариант команды работает некорректно. Я использовал следующую команду:

#./symtrace `ps -e | grep 'emacs' | cut -f 1 -d " "` strcpy

От редактора: объясняется это тем, что ключ '-e' команды ps выводит список процессов без параметров, с которыми они были вызваны, что снижает (по крайней мере, исключает сам grep из результатов), но не исключает вероятность дублирования информации (слегка утрируя):

#ps -e | grep 'log'
942 ?        00:00:00 syslogd
960 ?        00:00:00 klogd

3. Заключение.

Итак, мы подошли к концу последней части статьи, посвященной основам работы с ptrace. Как только вы окончательно поймете основную концепцию, то для вас не составит труда двинуться дальше. Более подробные сведения о ptrace и об ELF вы найдете на www.phrack.org. Еще я хотел бы заметить, что мы подошли к концу последней части ни разу не упомянув об одной важной особенности ptrace -- взаимодействии с системными вызовами. В User Mode Linux эта особенность используется очень широко. Сейчас я занят своей учебой и работой над курсовым проектом, но обещаю, что как только позволит время я вернусь к этой теме и мы продолжим рассмотрение особенностей ptrace.

Приветствуются любые предложения, замечания, дополнения и пр. Пишите мне по адресу: [email protected]


Copyright (C) 2002, Sandeep S. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 85 of Linux Gazette, December 2002

Вернуться на главную страницу