ptrace (от process trace) — системный вызов в некоторых unix-подобных системах (в том числе в Linux, FreeBSD, Max OS X), который позволяет трассировать или отлаживать выбранный процесс. Можно сказать, что ptrace дает полный контроль над процессом: можно изменять ход выполнения программы, смотреть и изменять значения в памяти или состояния регистров. Стоит оговориться, что никаких дополнительных прав при этом мы не получаем — возможные действия ограничены правами запущенного процесса. К тому же, при трассировке программы с setuid битом, этот самый бит не работает — привилегии не повышаются.
В статье будет показано, как перехватывать системные вызовы на примере ОС Linux.
Вот как выглядит прототип функции ptrace:
Напишем программу для вывода списка системных вызовов, используемых программой (простенький аналог утилиты strace).
Итак, для начала необходимо сделать fork — родительский процесс будет отлаживать дочерний:
В дочернем процессе все просто — начинаем трассировку с PTRACE_TRACEME и запускаем нужную программу:
При выполнении execl трассируемый процесс остановится, передав свое новое состояние родительскому. Поэтому родительский процесс сначала должен подождать запуска программы с помощью waitpid (можно просто wait, так как дочерний процесс всего один):
Чтобы как-то различать системные вызовы и другие остановки (например SIGTRAP), предусмотрен специальный параметр PTRACE_O_TRACESYSGOOD — при остановке на системном вызове родительский процесс получит в статусе SIGTRAP | 0x80:
Теперь можно в цикле выполнять PTRACE_SYSCALL до выхода из программы, и смотреть значение регистра eax для определения номера системного вызова. Для этого используем PTRACE_GETREGS. Следует отметить, что регистр eax в момент остановки заменен, и поэтому необходимо использовать сохраненный state.orig_eax:
Запустив программу, увидим нечто подобное:
Как видно, после системного вызова №4 (а это sys_write) выводится наш текст.
Попробуем теперь перехватить вызов, и сделать что-нибудь хорошее. Системный вызов write выглядит так:
Запускаем, и…
Таким образом, мы перехватили системный вызов sys_write в программе /bin/echo для вывода своего текста. Это всего лишь простой пример использования ptrace. С его помощью также можно легко делать дампы памяти (это, кстати, очень помогает при решении линуксовых крэкмисов), устанавливать breakpoint'ы (с помощью PTRACE_SINGLESTEP или заменой интсрукции на 0xCC), анализировать регистры/переменные и многое другое. ptrace очень полезен, например, когда до проблемного участка кода быстро не добраться — если в отладчике приходится многократно прыгать, подменять данные, а потом программа умирает и приходится все делать заново; если же написать программу для отладки ptrace'ом — все эти действия необходимо описать только один раз, и они будут выполняться автоматически. Конечно, в некоторых отладчиках можно писать скрипты — но по возможностям они навернякак уступают.
UPD: забыл выложить полный исходник
man ptrace
man wait
Playing with ptrace, part I
Playing with ptrace, part II
syscalls table
В статье будет показано, как перехватывать системные вызовы на примере ОС Linux.
1. Немного о ptrace
Вот как выглядит прототип функции ptrace:
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
- request — это действие, которое необходимо осуществить, например PTRACE_CONT, PTRACE_PEEKTEXT
- pid — индентификатор трассируемого процесса
- addr и data зависят от request'а
- PTRACE_SINGLESTEP — пошаговое выполнение программы, управление будет передаваться после выполнения каждой инструкции; такая трассировка достаточна медленна
- PTRACE_SYSCALL — продолжить выполнение программы до входа или выхода из системного вызова
- PTRACE_CONT — просто продолжить выполнение программы
2. Просмотр системных вызовов
Напишем программу для вывода списка системных вызовов, используемых программой (простенький аналог утилиты strace).
Итак, для начала необходимо сделать fork — родительский процесс будет отлаживать дочерний:
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid)
parent(pid);
else
child();
return 0;
}
В дочернем процессе все просто — начинаем трассировку с PTRACE_TRACEME и запускаем нужную программу:
void child() {
ptrace(PTRACE_TRACEME, 0, 0, 0);
execl("/bin/echo", "/bin/echo", "Hello, world!", NULL);
perror("execl");
}
При выполнении execl трассируемый процесс остановится, передав свое новое состояние родительскому. Поэтому родительский процесс сначала должен подождать запуска программы с помощью waitpid (можно просто wait, так как дочерний процесс всего один):
int status;
waitpid(pid, &status, 0);
Чтобы как-то различать системные вызовы и другие остановки (например SIGTRAP), предусмотрен специальный параметр PTRACE_O_TRACESYSGOOD — при остановке на системном вызове родительский процесс получит в статусе SIGTRAP | 0x80:
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD);
Теперь можно в цикле выполнять PTRACE_SYSCALL до выхода из программы, и смотреть значение регистра eax для определения номера системного вызова. Для этого используем PTRACE_GETREGS. Следует отметить, что регистр eax в момент остановки заменен, и поэтому необходимо использовать сохраненный state.orig_eax:
while (!WIFEXITED(status)) {
struct user_regs_struct state;
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
// at syscall
if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80) {
ptrace(PTRACE_GETREGS, pid, 0, &state);
printf("SYSCALL %d at %08lx\n", state.orig_eax, state.eip);
// skip after syscall
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
}
}
Запустив программу, увидим нечто подобное:
...
SYSCALL 6 at b783a430
SYSCALL 197 at b783a430
SYSCALL 192 at b783a430
SYSCALL 4 at b783a430
Hello, world!
SYSCALL 6 at b783a430
SYSCALL 91 at b783a430
SYSCALL 6 at b783a430
SYSCALL 252 at b783a430
Как видно, после системного вызова №4 (а это sys_write) выводится наш текст.
3. Перехват системного вызова
Попробуем теперь перехватить вызов, и сделать что-нибудь хорошее. Системный вызов write выглядит так:
write(fd, buf, n);
- ebx: fd — файловый дескриптор (номер)
- ecx: buf — указатель на текст для вывода
- edx: n — количество байт
// sys_write
if (state.orig_eax == 4) {
char * text = (char *)state.ecx;
ptrace(PTRACE_POKETEXT, pid, (void*)(text+7), 0x72626168); //habr
ptrace(PTRACE_POKETEXT, pid, (void*)(text+11), 0x00000a21); //!\n
}
Запускаем, и…
...
SYSCALL 6 at 00556416
SYSCALL 197 at 00556416
SYSCALL 192 at 00556416
SYSCALL 4 at 00556416
Hello, habr!
SYSCALL 6 at 00556416
SYSCALL 91 at 00556416
SYSCALL 6 at 00556416
SYSCALL 252 at 00556416
Таким образом, мы перехватили системный вызов sys_write в программе /bin/echo для вывода своего текста. Это всего лишь простой пример использования ptrace. С его помощью также можно легко делать дампы памяти (это, кстати, очень помогает при решении линуксовых крэкмисов), устанавливать breakpoint'ы (с помощью PTRACE_SINGLESTEP или заменой интсрукции на 0xCC), анализировать регистры/переменные и многое другое. ptrace очень полезен, например, когда до проблемного участка кода быстро не добраться — если в отладчике приходится многократно прыгать, подменять данные, а потом программа умирает и приходится все делать заново; если же написать программу для отладки ptrace'ом — все эти действия необходимо описать только один раз, и они будут выполняться автоматически. Конечно, в некоторых отладчиках можно писать скрипты — но по возможностям они навернякак уступают.
UPD: забыл выложить полный исходник
4. Что почитать
man ptrace
man wait
Playing with ptrace, part I
Playing with ptrace, part II
syscalls table