Как стать автором
Обновить

Перехват системных вызовов с помощью ptrace

Время на прочтение 4 мин
Количество просмотров 32K
ptrace (от process trace) — системный вызов в некоторых unix-подобных системах (в том числе в Linux, FreeBSD, Max OS X), который позволяет трассировать или отлаживать выбранный процесс. Можно сказать, что ptrace дает полный контроль над процессом: можно изменять ход выполнения программы, смотреть и изменять значения в памяти или состояния регистров. Стоит оговориться, что никаких дополнительных прав при этом мы не получаем — возможные действия ограничены правами запущенного процесса. К тому же, при трассировке программы с setuid битом, этот самый бит не работает — привилегии не повышаются.

В статье будет показано, как перехватывать системные вызовы на примере ОС 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_ATTACH), либо запустить его самому с помощью PTRACE_TRACEME. Мы рассмотрим второй случай, он немножко попроще, но суть та же. Для управления трассировкой можно использовать следующие аргументы:
  • PTRACE_SINGLESTEP — пошаговое выполнение программы, управление будет передаваться после выполнения каждой инструкции; такая трассировка достаточна медленна
  • PTRACE_SYSCALL — продолжить выполнение программы до входа или выхода из системного вызова
  • PTRACE_CONT — просто продолжить выполнение программы
Для более подробной информации — man ptrace.

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 — количество байт
Для подмены текста используем PTRACE_POKETEXT:

      // 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
Теги:
Хабы:
+51
Комментарии 9
Комментарии Комментарии 9

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн