В первой части мы с вами рассмотрели основы работы с ptrace. Во второй части был приведен пример небольшой программы, которая изменяла содержимое регистров процессора в другом приложении, заставляя его выполнить внедренный код. На этот раз мы будем учиться обращаться к памяти трассируемого процесса. Цель данной части состоит в том, чтобы продемонстрировать методику доступа к идентификаторам процесса во время исполнения. Область применения этой методики настолько широка, что ограничивается лишь вашей фантазией.
Мы уже знакомы с 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] и найти искомый идентификатор.
Теперь мы владеем основными сведениями, которые нам понадобятся,
можно начинать. Прежде всего начнем трассировку процесса
'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
Итак, мы подошли к концу последней части статьи, посвященной
основам работы с ptrace
. Как только вы окончательно
поймете основную концепцию, то для вас не составит труда двинуться
дальше. Более подробные сведения о ptrace и об ELF вы найдете на
www.phrack.org. Еще я хотел бы
заметить, что мы подошли к концу последней части ни разу не упомянув
об одной важной особенности ptrace -- взаимодействии с системными
вызовами. В User Mode Linux эта особенность используется очень
широко. Сейчас я занят своей учебой и работой над курсовым проектом,
но обещаю, что как только позволит время я вернусь к этой теме и мы
продолжим рассмотрение особенностей ptrace.
Приветствуются любые предложения, замечания, дополнения и пр. Пишите мне по адресу: [email protected]