Привет, Хабр! Сегодня у нас на столе инструмент, о котором многие слышали, но мало кто использовал по-настоящему — ptrace.
С ptrace можно подключаться к чужим процессам, читать и менять их память, перехватывать системные вызовы — и даже вежливо уволить sleep 9999.
Что такое ptrace и зачем он нужен
Когда вызывается ptrace(PTRACE_ATTACH, pid, …), мы не просто «подключаемся к процессу», а даем ядру команду установить отношение трассировки между двумя процессами. Это отношение фиксируется в task_struct обеих сторон: у родителя выставляется ptrace-флаг, у ребёнка — блокировка выполнения до следующего сигнала.
Фактически процесс становится дебаженным, и теперь каждый раз, когда он вызывает системный вызов или получает сигнал — он ставится на паузу и родитель (трассирующий процесс) уведомляется через waitpid(). Сам механизм очень похож на сигнал SIGSTOP, но с контролем через PTRACE_SYSCALL, PTRACE_SINGLESTEP и прочие вкусности.
Контроль через сигналы
ptrace работает вокруг сигнальной системы. Т.е каждый раз, когда «дитя» делает системный вызов, ядро ловит это и генерирует специальный SIGTRAP, на который реагирует трассирующий процесс. Как это выглядит:
-
Вызываем
PTRACE_SYSCALL, процесс продолжает выполнение до следующего системного вызова. -
Ядро перехватывает вход в
syscallи приостанавливает процесс. -
Родитель через waitpid ловит
WIFSTOPPED, понимает: ага, мы на входе в syscall. -
Хочешь — можно прочитать регистры, изменить аргументы вызова и т.д.
-
Повторяешь
PTRACE_SYSCALL, и процесс идёт дальше до выхода из syscall. -
Повторно ловишь
SIGTRAP— на выходе из вызова.
Можно подменить syscall прямо в orig_rax (x86_64) — и процесс выполнит совершенно другой вызов. Или подменить аргументы, написав в регистры rdi, rsi, rdx, если ты знаете ABI.
Когда мы цепляемся к процессу, ядро принудительно замораживает его. Именно поэтому, когда делаем PTRACE_ATTACH, нужнно обязательно нужно вызвать waitpid(pid, ...) — иначе оставим процесс в подвешенном состоянии, и он не сможет продолжить работу.
ptrace — опасная штука
Если трассирующий процесс не завершает цикл взаимодействия правильно — например, не продолжает выполнение целевого процесса через PTRACE_CONT или PTRACE_SYSCALL — то тот остаётся в состоянии stopped бесконечно. Это не просто паузится процесс: он блокирует ресурсы, может висеть в состоянии зомби или в T статусе в ps, пока за ним кто-то не придёт и не доделает цикл отладки до конца. Это классическая ошибка при написании наивных ptrace-трейсеров: повесился на процесс — и бросил, забыв, что теперь ты за него отвечаешь.
Вторая грань проблемы — если ты лезешь в память процесса и случайно затираешь критический участок (например, .text сегмент с инструкциями или .got.plt), то ты легко вызовешь SIGSEGV при следующем исполнении. Или хуже — модифицируешь return address на стеке, и получишь не просто краш, а непредсказуемое поведение. При этом, если ты криво реализуешь waitpid() — например, не вычитываешь все статусы (в т.ч WSTOPPED, WIFEXITED, WIFSIGNALED) — процесс зависнет или твой управляющий код потеряет поток исполнения. Ну и напоследок: ты не root — ты не лезешь в чужие процессы. Система проверяет UID/GID, а при попытке отладки setuid-бинарей вообще сбрасывает setuid-бит.
ptrace vs seccomp vs capabilities
Некоторые системы (особенно в контейнерах) ограничивают ptrace через:
-
yama/ptrace_scope=1: по дефолту запрещаетPTRACE_ATTACHмежду несвязанными процессами (можно обойти через родство); -
seccomp: может блокировать системные вызовы, даже если ты их подменяешь; -
capabilities: нужныCAP_SYS_PTRACE, если хочешь цепляться к чужим процессам вне user’а.
Запускаем процесс под трассировкой
Первый вариант — ты хочешь не вмешиваться в уже идущий процесс, а сам всё создать, обвязать, подцепить и проконтролировать. Сценарий классический: создаем дочерний процесс, который сам говорит ядру: «я согласен быть трассируемым». Это делается через PTRACE_TRACEME, и сразу после этого вызывается exec нужной программы.
Вот пример, где родитель не просто запускает, а перехватывает каждый системный вызов дочернего процесса:
#include <stdio.h> #include <stdlib.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/user.h> #include <unistd.h> #include <errno.h> int main() { pid_t child = fork(); if (child == 0) { // Дочерний процесс: объявляем себя трассируемым ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); perror("execl"); exit(1); } else { // Родитель: следим за каждым системным вызовом int status; waitpid(child, &status, 0); ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD); while (1) { ptrace(PTRACE_SYSCALL, child, NULL, NULL); waitpid(child, &status, 0); if (WIFEXITED(status)) break; if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80) { struct user_regs_struct regs; ptrace(PTRACE_GETREGS, child, NULL, ®s); printf("Syscall: %llu\n", regs.orig_rax); } } } return 0; }
fork() создаёт дочерний процесс. Он вызывает PTRACE_TRACEME, и ядро теперь будет ставить его на паузу при каждом системном вызове. Родитель снаружи слушает, что происходит, и печатает каждый системный вызов.
Подключаемся к уже живому процессу
Теперь второй способ: процесс уже работает, ты не хочешь его убивать, перезапускать — просто хочешь подглядеть. Тогда тебе нужен PTRACE_ATTACH:
#include <stdio.h> #include <stdlib.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <unistd.h> #include <errno.h> int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <pid>\n", argv[0]); return 1; } pid_t pid = atoi(argv[1]); if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) { perror("ptrace attach"); return 1; } waitpid(pid, NULL, 0); printf("Attached to process %d\n", pid); // Делаем что хотим… ptrace(PTRACE_DETACH, pid, NULL, NULL); printf("Detached\n"); return 0; }
После PTRACE_ATTACH процесс приостанавливается. Пока ты с ним не закончишь, он не продолжит выполнение. Поэтому всегда делаем PTRACE_DETACH или PTRACE_CONT — иначе он зависнет.
Чтение и запись памяти
Если хочется получить доступ к оперативке процесса:
long word = ptrace(PTRACE_PEEKTEXT, pid, (void *)addr, NULL); printf("Data at %p: 0x%lx\n", addr, word);
А теперь впишем туда свою строчку:
long new_word = 0x64636261; // 'abcd' в ASCII (LE) ptrace(PTRACE_POKETEXT, pid, (void *)addr, (void *)new_word);
Это прямой доступ в адресное пространство процесса. Главное знать точный адрес. Для этого пригодятся gdb, maps, nm, или просто ltrace.
Работа с регистрами
Работаешь на низком уровне? Снимаем и изменяем состояние процессора у чужого процесса:
#include <sys/user.h> struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, NULL, ®s); printf("RIP: %llx, RAX: %llx\n", regs.rip, regs.rax);
Изменим rip, чтобы перескочить инструкцию:
regs.rip += 2; ptrace(PTRACE_SETREGS, pid, NULL, ®s);
Здесь можно эмулировать выполнение, модифицировать параметры вызовов, делать всё, что может gdb, но вручную.
Убиваем sleep 9999 без kill
Итак, допустим:
sleep 9999 &
Он просто ждёт. Мы не хотим слать SIGKILL, а хотим заставить сам процесс вызвать exit(0). Для этого нужно:
-
Прикрепиться.
-
Подменить
rax(номер syscall) на 60 (это exit). -
В
rdiположить 0 (код выхода). -
Продолжить выполнение.
Реализация:
#include <stdio.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/user.h> #include <unistd.h> int main() { pid_t pid = 4242; // Подставь свой PID ptrace(PTRACE_ATTACH, pid, NULL, NULL); waitpid(pid, NULL, 0); struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, NULL, ®s); regs.rax = 60; // syscall exit regs.rdi = 0; // exit code 0 ptrace(PTRACE_SETREGS, pid, NULL, ®s); ptrace(PTRACE_CONT, pid, NULL, NULL); return 0; }
Процесс сам себя завершает через вызов exit(0), как будто дошёл до конца main. Никаких kill, никакого вмешательства в сигналы.
Спасибо за прочтение! Если вы уже использовали ptrace в своих задачах, расскажите об этом в комментариях.
Если вам близка идея ручного контроля над процессами, системными вызовами и инфраструктурой — вам точно будет интересно посмотреть, как этот подход масштабируется на уровне среды. На открытом уроке «Из песочницы в продакшен» разберём, как Ansible, Vagrant и Terraform помогают уходить от точечных решений к управляемой и воспроизводимой инфраструктуре. Без магии — только практика и системный подход.
ссылка на оригинал статьи https://habr.com/ru/articles/898448/
Добавить комментарий