From: Андрей Киселев <kis_an [at] linuxgazette [dot] ru>
Newsgroups: Russian Linux Gazette
Date: Mon, 17 May 2004 18:21:07 +0000 (UTC)
Subject: Трассировка процессов с помощью Ptrace
Трассировка процессов с помощью Ptrace -- Часть 1.
Автор: Sandeep S <http://www.linuxgazette.com/authors/sandeep.html>
Перевод: Андрей Киселев <kis_an [at] linuxgazette [dot] ru>
Системный вызов ptrace является основой основ для программ-отладчиков,
таких как gdb, но все же принципы работы с этим системным вызовом
недостаточно хорошо освещены в документации, если не считать, что
самой лучшей документацией являются исходные тексты ядра! Я постараюсь
продемонстрировать основные приемы при работе с ptrace и некоторые его
функциональные возможности, доступные в инструментальных средствах
подобных gdb.
1. Введение
-----------
ptrace() -- это системный вызов, который дает возможность одному
процессу управлять исполнением другого. Он так же позволяет изменять
содержимое памяти трассируемого процесса. Трассируемый процесс ведет
себя как обычно до тех пор пока не получит сигнал. Когда это
происходит, процесс переходит в состояние останова, а
процесс-трассировщик информируется об этом вызовом wait(). После этого
процесс-трассировщик, через вызовы ptrace, определяет реакцию
трассируемого процесса. Исключение составляет сигнал SIGKILL, который
само собой разумеется, уничтожает процесс.
Кроме того, можно задать переход трассируемого процесса в состояние
останова по определенному событию, которое возникло в ходе его
исполнения. Это происходит только в том случае, если
процесс-трассировщик установил какие-либо флаги событий в контексте
трассируемого процесса. Трассировщик может даже завершить трассируемый
процесс, установив при этом код его завершения. После выполнения
каких-либо действий трассировщик может завершить отлаживаемый процесс
или продолжить его исполнение.
Обратите внимание: Ptrace() очень сильно зависит от аппаратной
архитектуры. Приложения, использующие ptrace, очень тяжело переносятся
на другие аппаратные платформы.
2. Подробнее
------------
Объявление ptrace() выглядит следующим образом.
______________________________________________________________
#include <sys/ptrace.h>
long int ptrace(enum __ptrace_request request, pid_t pid,
void * addr, void * data)
______________________________________________________________
Вызову передаются четыре аргумента, где request -- определяет что
необходимо сделать. pid -- это ID трассируемого процесса. addr -- это
смещение в пользовательском пространстве трассируемого процесса,
откуда будет прочитано слово данных и возвращено в качестве результата
работы вызова.
Родительский процесс может породить дочерний процесс и выполнять его
трассировку посредством вызова ptrace с аргументом request, имеющим
значение PTRACE_TRACEME. Процесс-трассировщик может выполнять
трассировку уже существующего процесса, используя значение
PTRACE_ATTACH для аргумента request. Значения, которые может принимать
аргумент request, обсуждаются ниже.
2.Как работает ptrace().
------------------------
Всякий раз, когда вызывается ptrace, в первую очередь выполняется
блокировка ядра. А непосредственно перед возвратом блокировка
снимается. Рассмотрим работу ptrace() для различных значений аргумента
request.
PTRACE_TRACEME:
Это значение используется при трассировке дочернего процесса. Как уже
говорилось выше, любой сигнал (за исключением SIGKILL), как
поступивший от системы, так и поступивший через вызов exec, от самого
процесса, вынуждает процесс перейти в состояние останова, что
позволяет "родителю" определять дальнейший ход развития событий.
Единственное, что делает ptrace() в этом случае -- проверяет,
установлен ли флаг PT_PTRACED для текущего процесса. Если флаг не
установлен, то проверяются права доступа и флаг устанавливается. Все
остальные аргументы игнорируются.
PTRACE_ATTACH:
Это значение используется в том случае, если необходимо выполнить
трассировку существующего процесса. Единственное замечание: ни один
процесс не сможет получить контроль над процессом init или над самим
собой. Трассировка этих процессов является недопустимой. Выполнив
вызов ptrace, с этим значением аргумента request, процесс становится
"родителем" для процесса с ID равным pid. Однако, вызов getpid(),
выполняемый "дочерним" процессом, по-прежнему будет возвращать PID
реального родителя.
После обычной проверки прав доступа, проверяется -- не производится ли
попытка получить контроль над процессом init или над самим собой, не
установлен ли флаг PT_PTRACED. Если проблем не возникло, то
устанавливается флаг PT_PTRACED. Затем исправляются ссылки
трассируемого процесса, например, он удаляется из очереди задач, поле
ссылки на родительский процесс изменяется (подлинный родитель остается
тем же самым). Процесс снова помещается в очередь и ему передается
сигнал SIGSTOP. Аргументы addr и data игнорируются.
PTRACE_DETACH:
Прекращает трассировку процесса. В этот момент принимается решение о
прекращении или продолжении работы трассируемого процесса. Отменяются
все изменения, произведенные по PTRACE_ATTACH/PTRACE_TRACEME. Через
аргумент data устанавливается код завершения. В поле связи, у
трассируемого процесса, восстанавливается ссылка на настоящего
родителя. Сбрасывается бит пошаговой отладки. И наконец, трассируемый
процесс "пробуждается". Аргумент addr игнорируется.
При значениях аргумента request PTRACE_PEEKTEXT и PTRACE_PEEKDATA,
родительскому процессу возвращается слово, находящееся по адресу addr
в адресном пространстве трассируемого (дочернего) процесса. Оба эти
значения request приводят к одинаковым результатам. В случае
PTRACE_PEEKUSER - читается слово из структуры типа user (см.
sys/user.h), размещенной в системном адресном пространстве и
соответствующей трассируемому процессу. Аргумент addr задает смещение
от начала структуры. Прочитанное слово возвращается через аргумент
data. В случае успеха возвращается 0. Исходное значение аргумента data
игнорируется.
При значениях request, PTRACE_POKETEXT и PTRACE_POKEDATA, производится
запись значения аргумента data по адресу addr в пространстве
трассируемого процесса. Оба эти значения приводят к одинаковым
результатам.
В случае PTRACE_POKEUSER, производится запись в структуру типа user,
соответствующей трассируемому процессу. Следует быть очень осторожным
при работе с этим параметром, поскольку в данном случае мы вторгаемся
в область ядра. После выполнения большого количества проверок, ptrace
выполняет запись в указанную позицию структуры, при этом доступными
для записи оказываются далеко не все элементы структуры. Аргумент addr
в данном случае определяет смещение относительно начала структуры.
PTRACE_SYSCALL, PTRACE_CONT:
Обе эти команды активируют трассируемый процесс. В случае
PTRACE_SYSCALL дочернему процессу предписывается остановиться на
следующем системном вызове. PTRACE_CONT -- просто возобновляет работу
трассируемого процесса. И в том и в другом случае, если аргумент data
не равен нулю или SIGSTOP, ptrace() передает его процессу как сигнал,
который необходимо обработать. При этом ptrace() сбрасывает бит
пошаговой трассировки и устанавливает/сбрасывает бит трассировки
системных вызовов. Аргумент addr игнорируется.
PTRACE_SINGLESTEP:
Имеет тот же смысл, что и PTRACE_SYSCALL, за исключением того, что
трассируемый процесс останавливается после исполнения каждой
инструкции. Устанавливает бит пошаговой трассировки. Как и выше,
аргумент data содержит код завершения для трассируемого процесса.
Аргумент addr игнорируется.
PTRACE_KILL:
Используется для завершения трассируемого процесса. Завершение
производится следующим образом. Ptrace() проверяет -- "жив" ли
трассируемый процесс, затем устанавливает код завершения дочернего
процесса в значение sigkill, сбрасывает бит пошаговой трассировки и
активирует дочерний процесс, который в соответствии с кодом завершения
прекращает свою работу.
2.2 Аппаратно-зависимые значения для аргумента request
------------------------------------------------------
Описанные выше значения аргумента request являются
аппаратно-независимыми. Значения, описываемые ниже, позволяют
читать/изменять регистры процессора дочернего процесса, а потому очень
тесно связаны с аппаратной реализацией системы. Набор доступных
регистров включает в себя регистры общего назначения и регистры FPU
(арифметического сопроцессора).
При этих значениях request, после обычной проверки прав доступа,
производится копирование значений регистров общего назначения,
регистров с плавающей точкой, дополнительных регистров с плавающей
точкой дочернего процесса в переменную родительского процесса data.
Копирование выполняется с помощью функций getreg() и __put_user(),
аргумент addr игнорируется.
При этих значениях аргумента request выполняется запись в регистры
процессора трассируемого процесса. В данном случае доступ к отдельным
регистрам ограничивается. Значения регистров берутся из аргумента
data. Аргумент addr игнорируется.
2.3 Возвращаемые значения системного вызова ptrace()
----------------------------------------------------
В случае успеха ptrace() возвращает ноль. В случае возникновения
ошибки -- возвращается значение -1, а код ошибки -- в переменной
errno. Поскольку при выполнении операций PEEKDATA/PEEKTEXT, даже в
случае успеха может быть возвращено значение -1, то лучше выполнять
проверку на наличие ошибки по переменной errno. Коды ошибок могут быть
следующими
EPERM : Отсутствие прав доступа.
ESRCH : Требуемый процесс не найден или уже трассируется.
EIO : Недопустимый код запроса (request) или задан недопустимый адрес
памяти для чтения/записи.
EFAULT : Была сделана попытка записи информации в область памяти, но
скорее всего эта память не существует или недоступна.
К сожалению, зачастую ошибки EIO и EFAULT порождаются практически
идентичными ситуациями, из-за чего очень сложно интерпретировать
разницу между ними.
3. Небольшой пример.
--------------------
Если вы нашли описание аргументов чересчур сухим, не отчаивайтесь.
Далее я попробую представить ряд маленьких программ, которые
проиллюстрируют все вышесказанное.
Вот первый пример. Здесь родительский процесс подсчитывает число
инструкций, выполненных тестовой программой, которая запускается как
дочерний процесс.
Тестовая программа выводит содержимое текущего каталога и подсчитывает
количество затраченных машинных инструкций.
______________________________________________________________
int main(void)
{
long long counter = 0; /* Счетчик машинных инструкций */
int wait_val; /* значение, возвращаемое потомком */
int pid; /* pid потомка */
puts("Минутку терпения");
switch (pid = fork()) {
case -1:
perror("fork");
break;
case 0: /* запуск дочернего процесса */
ptrace(PTRACE_TRACEME, 0, 0, 0);
/*
* необходимо, чтобы передать
* управление дочернему процессу
*/
execl("/bin/ls", "ls", NULL);
/*
* выполнить программу и заставить
* потомка остановиться и передать сигнал
* родителю, теперь родитель
* сможет перейти в PTRACE_SINGLESTEP
*/
break;
/* завершение дочернего процесса */
default:/* запуск родительского процесса */
wait(&wait_val);
/*
* родитель ожидает, пока потомок не остановится
* на следующей инструкции (execl())
*/
while (wait_val == 1407 ) {
counter++;
if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0) != 0)
perror("ptrace");
/*
* переход в пошаговый режим
* и активация потомка
*/
wait(&wait_val);
/* ожидание выполнения следующей инструкции */
}
/*
* цикл продолжается до тех пор, пока
* потомок не завершит работу; wait_val != 1407
* младший байт = 0177L и старший = 05 (SIGTRAP)
*/
}
printf("Количество машинных инструкций : %lldn", counter);
return 0;
}
______________________________________________________________
Скопируйте текст программы в текстовый редактор, сохраните ее в файл
file.c и дайте команды на выполнение:
cc file.c
./a.out
В результате работы программы, на экран будут выведены содержимое
текущего каталога и количество затраченных машинных инструкций. Теперь
попробуйте перейти в другой каталог и запустить программу оттуда.
Сравните полученные результаты. (Обратите внимание, если у вас
медленная машина, то вывод может занять довольно продолжительное
время). (На P4 1.7 ГГц на это ушло около 7 секунд. Прим.ред.)
4. Заключение
-------------
Ptrace() -- это средство отладки программ. Он может использоваться и
для трассировки системных вызовов. Родительский процесс может начать
трассировку, вызвав сначала функцию fork(2), для запуска дочернего
процесса, а затем дочерний процесс может выполнить PTRACE_TRACEME, за
которым (как правило) следует выполнение exec(3) (в примере выше --
это программа "ls"). Затем, после выполнения каждой инструкции,
родитель может просматривать значения регистров потомка, данные в
памяти и влиять на протекание процесса исполнения. В следующей части
статьи я приведу пример программы, которая использует различные
особенности ptrace(). До скорой встречи!
Трассировка процессов с помощью Ptrace -- Часть 2
Оригинал: http://gazette.linux.ru.net/lg83/sandeep.html
В первой части мы узнали об основных особенностях ptrace и
рассмотрели небольшой пример. Как я уже говорил ранее, практически
всегда процессы-отладчики обращаются к памяти или регистрам процессора
трассируемого приложения. Теперь бы я хотел осветить строение
исполняемых файлов, чтобы вы получили представление о том что и где в
них находится. Здесь я расскажу о формате исполняемых файлов ELF,
используемом в Linux. А в последнем разделе этой части я представлю
небольшой пример программы, которая изменяет содержимое памяти и
регистров процессора другой программы, и внедряет в ее тело некоторый
дополнительный исполняемый код.
Обратите внимание: Пусть вас не смущает такое вступление. Эта часть
статьи, вне всякого сомнения, рассказывает о ptrace, а не об ELF. Но
знание формата ELF определенно необходимо, чтобы уметь обращаться к
памяти трассируемого процесса. Итак, приступим.
1. Что такое ELF?
-----------------
ELF -- это Executable and Linking Format (Формат Исполняемых и
Связываемых файлов). Он определяет формат двоичных исполняемых файлов,
объектных файлов, разделяемых объектов (библиотек), а так же файлов
core dump. Формат ELF используется как компоновщиками (linkers), так и
загрузчиками программ, хотя каждый из них интерпретирует ELF-файлы
по-своему.
ELF-файл состоит из множества секций и сегментов. Объектные файлы
имеют таблицу заголовков секций, исполняемые -- таблицу программных
заголовков, а разделяемые библиотеки -- и то и другое. В следующем
разделе я познакомлю вас с этими заголовками поближе.
2. Заголовки ELF
----------------
Любой ELF-файл имеет ELF-заголовок. Заголовок всегда размещается в
самом начале файла. Он содержит описание двоичного файла, определяя
таким образом порядок интерпретации файла.
Структура заголовка приведена ниже (см. /usr/src/include/linux/elf.h)
(путь меняется в зависимости от дистрибутива -- прим.ред.)
Кратко опишу поля структуры
1. e_ident : Сигнатура и прочая информация. Зависит от аппаратной
платформы.
2. e_type : Содержит информацию о типе файла. Тип может быть одним из
следующих: "объектный", "исполняемый", "разделяемый" (shared
object) и "core".
3. e_machine : Вы наверняка уже догадались, что это поле определяет
аппаратную архитектуру -- Intel 386, Alpha, Sparc и т.п.
4. e_version : Версия объектного файла.
5. e_phoff : Смещение до первого программного заголовка.
6. e_shoff : Смещение до первого заголовка секции.
7. e_flags : Флаги процессора. Не используется для i386
8. e_ehsize : Размер ELF-заголовка в байтах.
9. e_phentsize & e_shentsize : Размер программного заголовка и
заголовка секции, в таблицах программных заголовков и заголовков
секций соответственно.
10. e_phnum & e_shnum : Количество программных заголовков и заголовков
секций в соответствующих таблицах.
11. e_shstrndx : В таблице заголовков секций есть секция, которая
содержит имена других секций. Это индекс такой секции в таблице.
(см. ниже)
3. Секции и Сегменты
--------------------
Как я уже упоминал выше, компоновщики интерпретируют файл как
множество секций, описанных в таблице заголовков секций, а загрузчик
-- как множество сегментов, описанных в таблице программных
заголовков. В этом разделе приводится более или менее подробное
описание заголовков секций и сегментов.
3.1 Секции и заголовки секций
-----------------------------
Двоичный файл выглядит как набор секций, каждую из которых можно
представить в виде массива байт. Даже при наличии дополнительной
информации, помогающей корректно интерпретировать содержимое секции,
приложения могут выполнять интерпретацию по-своему.
Таблица заголовков секций представляет из себя массив заголовков.
Нулевой элемент массива всегда пуст и не соответствует ни одной из
секций. Каждый заголовок секции имеет следующий формат (см.
/usr/src/include/linux/elf.h):
______________________________________________________________
typedef struct elf32_shdr {
Elf32_Word sh_name; /* Имя секции, индекс в таблице строк (Elf32) */
Elf32_Word sh_type; /* Тип секции (Elf32) */
Elf32_Word sh_flags; /* Различные атрибуты секции */
Elf32_Addr sh_addr; /* Виртуальный адрес секции */
Elf32_Off sh_offset; /* Смещение от начала файла */
Elf32_Word sh_size; /* Размер секции в байтах */
Elf32_Word sh_link; /* Индекс следующей секции (Elf32) */
Elf32_Word sh_info; /* Дополнительные сведения о секции (Elf32) */
Elf32_Word sh_addralign; /* Выравнивание секции */
Elf32_Word sh_entsize; /* Размер записи в таблице */
} Elf32_Shdr;
______________________________________________________________
Теперь о полях структуры более подробно.
1. sh_name : Индекс строки в секции, содержащей таблицу строк
e_shstrndx. Указывает на начало строки, завершающейся нулевым
(0x00) символом, которая используется в качестве имени секции.
+ .text -- Эта секция содержит инструкции, исполняемые
процессором
+ .data -- В этой секции находятся инициализированные данные
программы.
+ .init -- Эта секция содержит инструкции, исполняемые
процессором при запуске программы.
2. sh_type : Тип секции, например, данные, таблица символов, таблица
строк и т.п..
3. sh_flags : Содержит вспомогательную информацию, определяющую
порядок интерпретации содержимого секции.
4. sh_addralign : Содержит размер выравнивания для секции, обычно 0,
1 (оба означают отсутствие выравнивания) или 4.
Смысл назначения остальных полей структуры достаточно прозрачно
следует из их названий.
3.2 Сегменты и программные заголовки.
-------------------------------------
Сегменты используются в процессе загрузки программы, т.е. при создании
образа процесса в памяти. Каждый сегмент описывается соответствующим
программным заголовком. Таблица представляет из себя массив
заголовков. Каждый программный заголовок имеет следующий формат:
______________________________________________________________
typedef struct
{
Elf32_Word p_type; /* Тип сегмента */
Elf32_Off p_offset; /* Смещение от начала файла */
Elf32_Addr p_vaddr; /* Виртуальный адрес сегмента */
Elf32_Addr p_paddr; /* Физический адрес сегмента */
Elf32_Word p_filesz; /* Размер сегмента в файле */
Elf32_Word p_memsz; /* Размер сегмента в памяти */
Elf32_Word p_flags; /* Флаги сегмента */
Elf32_Word p_align; /* Выравнивание сегмента */
} Elf32_Phdr;
______________________________________________________________
1. p_type : Определяет тип сегмента, т.е. задает порядок его
интерпретации, например:
+ неиспользуемый (unused)
+ загружаемый (loadable)
+ Информация для динамического связывания
+ зарезервировано (reserved)
и т.п..
2. p_vaddr : относительный виртуальный адрес загрузки сегмента.
3. p_paddr : физический адрес загрузки сегмента.
4. p_flags : Содержит флаги прав доступа -- чтение/запись/исполнение
5. p_align : Выравнивание сегмента в памяти. Если сегмент имеет тип
"загружаемый" (loadable), то он выравнивается по границе страницы
памяти.
Смысл назначения остальных полей структуры понятен из их названий.
4. Загрузка ELF-файла
---------------------
Мы уже имеем представление о структуре ELF-файла. Теперь перейдем к
рассмотрению порядка загрузки файла. Обычно, для того чтобы запустить
программу мы набираем ее имя в командной строке. На самом деле, после
того как мы нажмем на клавишу RETURN (или, если хотите -- ENTER),
происходит масса интересных вещей.
Прежде всего командная оболочка вызывает стандартную функцию из libc,
которая в свою очередь обращается к ядру. Теперь в игру вступает ядро.
Ядро открывает файл и определяет тип файла как исполняемый. Затем
загружает файл и все необходимые библиотеки, инициализирует стек и
передает управление загруженной программе.
Программа загружается по адресу 0x08048000 (см. /proc/pid/maps), а
стек начинается с адреса 0xBFFFFFFF (стек "растет" в сторону меньших
адресов).
5. Внедрение кода
-----------------
Теперь, когда процесс загружен в память и нам известно его адресное
пространство, мы можем выполнять трассировку этого процесса (при
наличии прав доступа) и просматривать/изменять данные в памяти
процесса. Однако сказать легко, сделать -- сложнее. Тем не менее,
почему бы не попробовать?
Прежде всего, давайте попробуем создать программу, которая могла бы
читать/писать в регистры процессора другой программы. Для аргумента
request мы будем использовать следующие значения.
* PTRACE_ATTACH : Начать трассировку существующего процесса с
заданным pid.
* PTRACE_DETACH : Завершить трассировку процесса с заданным pid.
Важно : Не следует забывать о необходимости этого вызова, иначе
процесс останется в режиме останова.
* PTRACE_GETREGS : Скопировать содержимое регистров процессора в
структуру, адрес которой передается в аргументе data (аргумент
addr игнорируется). Эта структура (struct user_regs_struct)
определена в файле asm/user.h.
______________________________________________________________
struct user_regs_struct {
long ebx, ecx, edx, esi, edi, ebp, eax;
unsigned short ds, __ds, es, __es;
unsigned short fs, __fs, gs, __gs;
long orig_eax, eip;
unsigned short cs, __cs;
long eflags, esp;
unsigned short ss, __ss;
};
______________________________________________________________
* PTRACE_SETREGS : Скопировать данные из структуры, адрес которой
передается в аргументе data, в регистры процессора.
* PTRACE_POKETEXT : Скопировать 32-битное слово из адреса, который
передается в аргументе data, в область памяти трассируемого
процесса, адресуемой аргументом addr.
Теперь вставим некоторый код в тело трассируемого процесса и заставим
процесс исполнить его, изменив содержимое регистра eip (instruction
pointer).
Делается это довольно просто. Для начала мы запускаем трассировку
процесса, затем считываем содержимое регистров. Куда-нибудь в стек
вставляем наш код, который мы хотим исполнить и изменяем содержимое
регистра eip таким образом, чтобы он указывал на первую инструкцию
нашего кода. После чего завершаем трассировку процесса. После этих
действий трассируемый процесс приступит к исполнению нашего
внедренного кода.
Пример состоит из трех файлов с исходными текстами. Первый -- это сам
трассировщик. Второй -- это код на языке Ассемблера, который будет
вставляться в тело трассируемого процесса. И третий -- небольшая
программка, которая будет подвержена нашим экспериментам.
Перейдите в другую консоль и запустите программу loop:
#./loop
Вернитесь обратно и запустите трассировщик:
#./catch `ps ax | grep "loop" | cut -f 2 -d ' '`
Теперь вернитесь в консоль, в которой была запущена программа 'loop' и
увидите что произошло! Итак! Ваши игры с ptrace начались!
От переводчика: В программу Tracer.c мною были внесены изменения. В
оригинальном варианте программа loop при исполнении внедренного кода
выводила сообщение "Oh, Caught!". Я взял на себя смелость заменить его
текстом "Во! Поймали!". Однако текст "зашит" в кодировке koi8-r,
поэтому, если у вас локаль настроена на иную кодировку, то вы увидите
это сообщение в искаженном виде. Оригинальный вариант файла
Tracer.c находится здесь (http://gazette.linux.ru.net/lg83/misc/sandeep/Tracer.c.orig).
6. Забегая вперед
-----------------
В первой части статьи мы подсчитали количество ассемблерных
инструкций, выполненных программой. В этой части мы рассмотрели
структуру исполняемого файла и попробовали вставить свой код в тело
"подопытного" процесса. В следующей части я покажу как получить доступ
к памяти трассируемого процесса. До скорых встреч! Sandeep S.
Трассировка процессов с помощью Ptrace -- Часть 3
Оригинал: http://gazette.linux.ru.net/lg85/sandeep.html
В первой части мы с вами рассмотрели основы работы с 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; /* Ссылки на загруженные объекты. */
};
Краткое описание полей структуры.
* l_addr: Базовый адрес памяти, куда был загружен объект. Это
значение можно найти в /proc/<pid>/maps
* l_name: указатель на имя библиотеки в таблице строк
* l_ld :указатель на динамическую (DT_*) секцию разделяемой
библиотеки
* 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.
Назначение функций очевидно из их названий.
Поиск начинается с адреса 0x08048000, где размещен ELF-заголовок
трассируемого процесса. По содержимому ELF-заголовка определяется
местоположение программного заголовка. (Структура заголовков
обсуждалась во второй части статьи.) После того как будет получен
программный заголовок, выполняется поиск заголовка с информацией о
динамическом связывании. Затем по полученной информации выполняется
поиск базового адреса глобальной таблицы смещений.
После того как получен адрес таблицы -- из нее читается второй
элемент, где находится адрес link_map, а из полученного адреса
читается содержимое link_map, которое и возвращается в качестве
результата.
Мы получили в свое распоряжение struct link_map, а теперь надо
получить таблицу символов (symtab) и таблицу строк (strtab). Для этого
обратимся к полю l_ld структуры link_map и пройдемся по набору
динамических секций, пока не обнаружим секции DT_SYMTAB и DT_STRTAB, в
этих секциях мы как раз и попытаемся обнаружить искомые
идентификаторы.
Эта функция проходит по динамическим секциям, проверяя каждую -- не
содержит ли она признак DT_STRTAB или DT_SYMTAB. Если секция имеет
один из этих признаков, то адрес таблицы запоминается в
соответствующем указателе strtab или symtab. Цикл завершается после
того, как будут просмотрены все динамические секции.
Следующий наш шаг -- получить значение идентификатора из таблицы
символов. Для этого проверяются все элементы таблицы идентификаторов,
не являются ли они именами функций. (Нас интересует возможность
обнаружения библиотечной функции). Если данный идентификатор -- имя
функции, то затем он сравнивается с заданным именем функции. Если они
совпадают, то возвращается значение идентификатора.
Теперь мы получили значение идентификатора, что собственно и
требовалось. Как его можно использовать? Ответ на этот вопрос зависит
уже от вас, уважаемый читатель. Как и все в этом мире, его можно
использовать как во благо так и во вред.
У вас может сложиться впечатление, что на этом мы закончили -- но это
не так. Мы забыли выполнить еще один обязательный шаг -- "отпустить"
приложение, т.е. закончить трассировку. Если этого не сделать, то
трассируемое приложение останется в состоянии останова "на веки
вечные", последствия этого мы частично рассматривали в [6]части I. Так
что прежде чем завершить работу мы завершаем трассировку.
Пример программы вы найдете в файле Ptrace.c
http://gazette.linux.ru.net/lg85/misc/sandeep/Ptrace.c.txt
Соберите программу командой
#cc Ptrace.c -o symtrace
А теперь протестируем ее. Запустите какое либо приложение (в другом
терминале) и подайте следующую команду. (Здесь я хочу уточнить, что в
качестве "подопытного" приложения я запускал emacs и искал
идентификатор strcpy). Вы можете выбрать для экспериментов любое
другое приложение и попытаться отыскать в нем любой другой
идентификатор.
От переводчика: на моей системе этот вариант команды работает
некорректно. Я использовал следующую команду:
#./symtrace `ps -e | grep 'emacs' | cut -f 1 -d " "` strcpy
От редактора: объясняется это тем, что ключ '-e' команды ps выводит
список процессов без параметров, с которыми они были вызваны, что
снижает (по крайней мере, исключает сам grep из результатов), но не
исключает вероятность дублирования информации (слегка утрируя):
Итак, мы подошли к концу последней части статьи, посвященной основам
работы с ptrace. Как только вы окончательно поймете основную
концепцию, то для вас не составит труда двинуться дальше. Более
подробные сведения о ptrace и об ELF вы найдете на [8]www.phrack.org.
Еще я хотел бы заметить, что мы подошли к концу последней части ни
разу не упомянув об одной важной особенности ptrace -- взаимодействии
с системными вызовами. В User Mode Linux эта особенность используется
очень широко. Сейчас я занят своей учебой и работой над курсовым
проектом, но обещаю, что как только позволит время я вернусь к этой
теме и мы продолжим рассмотрение особенностей ptrace.
Приветствуются любые предложения, замечания, дополнения и пр. Пишите
мне по адресу: busybox at sancharnet.in
Copyright (C) 2002, Sandeep S. Copying license
http://www.linuxgazette.com/copying.html
Published in Issue 85 of Linux Gazette, December 2002
/* прочитать данные из указанного адреса */
void read_data(int pid, unsigned long addr, void *vptr, int len)
{
int i, count;
long word;
unsigned long *ptr = (unsigned long *) vptr;
count = i = 0;
while (count < len) {
word = ptrace(PTRACE_PEEKTEXT, pid, addr + count, NULL);
count += 4;
ptr[i++] = word;
}
}
/* прочитать строку из памяти процесса */
char *read_str(int pid, unsigned long addr, int len)
{
char *ret = calloc(32, sizeof(char));
read_data(pid, addr, ret, len);
return ret;
}
/* записать данные по указанному адресу */
void write_data(int pid, unsigned long addr, void *vptr, int len)
{
int i, count;
long word;
i = count = 0;
while (count < len) {
memcpy(&word, vptr + count, sizeof(word));
word = ptrace(PTRACE_POKETEXT, pid, addr + count, word);
count += 4;
}
}
/* поиск структуры 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;
/*
* для начала найти в elf-заголовке, расположенному по адресу 0x08048000,
* таблицу программных заголовков, откуда мы попробуем
* извлечь информацию о секции PT_DYNAMIC.
*/
got = (Elf32_Word) dyn->d_un.d_ptr;
got += 4; /* second GOT entry, remember? */
/*
* теперь просто прочитать первую запись в link_map и вернуть ее
*/
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;
}