Aventura con traza (2)

Sobre Habré ya escribió sobre la intercepción de llamadas al sistema por medio de ptrace ; Alexa escribió sobre esta publicación mucho más detallada, que decidí traducir.


Por donde empezar


La comunicación entre el programa depurado y el depurador se produce mediante señales. Esto complica enormemente las cosas ya difíciles; para divertirse, puede leer la sección BUGS de man ptrace .

Hay al menos dos formas diferentes de comenzar a depurar:

  1. ptrace(PTRACE_TRACEME, 0, NULL, NULL) convertirá al padre del proceso actual en un depurador. No se requiere asistencia de los padres; man aconseja suavemente: "Un proceso probablemente no debería hacer esta solicitud si su padre no espera rastrearla". (¿En otra parte del hombre vio la frase "probablemente no debería" ?) Si el proceso actual ya tenía un depurador, la llamada fallará.
  2. ptrace(PTRACE_ATTACH, pid, NULL, NULL) hará que el proceso actual sea un depurador para pid . Si pid ya tenía un depurador, la llamada fallará. SIGSTOP envía al proceso depurado y no continuará funcionando hasta que el depurador lo descongele.

Estos dos métodos son completamente independientes; Puede usar uno u otro, pero no tiene sentido combinarlos. Es importante tener en cuenta que PTRACE_ATTACH no es instantáneo: después de ptrace(PTRACE_ATTACH) , generalmente se llama a waitpid(2) para esperar hasta que PTRACE_ATTACH "funcione".

Puede iniciar el proceso secundario en depuración utilizando PTRACE_TRACEME siguiente manera:

 static void tracee(int argc, char **argv) { if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) die("child: ptrace(traceme) failed: %m"); /*   ,   . */ if (raise(SIGSTOP)) die("child: raise(SIGSTOP) failed: %m"); /*  . */ execvp(argv[0], argv); /*     . */ die("tracee start failed: %m"); } static void tracer(pid_t pid) { int status = 0; /* ,       . */ if (waitpid(pid, &status, 0) < 0) die("waitpid failed: %m"); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { kill(pid, SIGKILL); die("tracer: unexpected wait status: %x", status); } /*      ptrace,    . */ /* *  ,      *  ,      . *    --  API  ptrace! */ /*       PTRACE_SYSCALL. */ } /* (argc, argv) --    ,    . */ void shim_ptrace(int argc, char **argv) { pid_t pid = fork(); if (pid < 0) die("couldn't fork: %m"); else if (pid == 0) tracee(argc, argv); else tracer(pid); die("should never be reached"); } 

Sin una llamada de raise(SIGSTOP) , podría resultar que execvp(3) se ejecute antes de que el proceso padre esté listo para esto; y luego las acciones del depurador (por ejemplo, interceptar llamadas del sistema) no comenzarán desde el comienzo del proceso.

Cuando se inicia la depuración, cada ptrace(PTRACE_SYSCALL, pid, NULL, NULL) "descongelará" el proceso depurado hasta la primera entrada en la llamada al sistema, y ​​luego hasta que la llamada del sistema se vaya.

Ensamblador telequinético


ptrace(PTRACE_SYSCALL) no devuelve ninguna información al depurador; simplemente promete que el proceso que se está depurando se detendrá dos veces en cada llamada al sistema. Para obtener información sobre lo que está sucediendo con el proceso depurado, por ejemplo, en qué sistema se detuvo, debe acceder a una copia de sus registros almacenados por el núcleo en un struct user en un formato que depende de la arquitectura específica. (Por ejemplo, en x86_64, el número de llamada estará en el campo regs.orig_rax , el primer parámetro pasado estará en regs.rdi , etc.) Alexa comenta: "se siente como si estuvieras escribiendo código de ensamblaje en C que funciona con los registros del procesador remoto".

En lugar de la estructura descrita en sys/user.h , puede ser más conveniente usar las constantes de índice definidas en sys/reg.h :

 #include <sys/reg.h> /*    . */ long ptrace_syscall(pid_t pid) { #ifdef __x86_64__ return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX); #else // ... #endif } /*      . */ uintptr_t ptrace_argument(pid_t pid, int arg) { #ifdef __x86_64__ int reg = 0; switch (arg) { case 0: reg = RDI; break; case 1: reg = RSI; break; case 2: reg = RDX; break; case 3: reg = R10; break; case 4: reg = R8; break; case 5: reg = R9; break; } return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL); #else // ... #endif } 

En este caso, dos paradas del proceso de depuración, a la entrada de la llamada del sistema y a la salida de la misma, no difieren en modo alguno desde el punto de vista del depurador; por lo tanto, el depurador debe recordar en qué estado se encuentra cada uno de los procesos depurados: si hay varios, entonces nadie garantiza que un par de señales de un proceso vendrán en una fila.

Descendientes


Una de las opciones de ptrace , a saber, PTRACE_O_TRACECLONE , garantiza que todos los elementos secundarios del proceso depurado se PTRACE_O_TRACECLONE automáticamente cuando salgan de fork(2) . Un punto sutil adicional aquí es que los descendientes tomados para la depuración se convierten en "pseudo-hijos" del depurador, y waitpid responderá no solo para detener a "hijos inmediatos", sino también para detener la depuración de "pseudo-hijos". El hombre advierte sobre esto: "No se recomienda configurar el indicador WCONTINUED cuando se llama a waitpid (2): el estado" continuado "es por proceso y consumirlo puede confundir al padre real del rastro". - es decir Los "pseudo-niños" tienen dos padres que pueden esperar a que se detengan. Para el programador del depurador, esto significa que waitpid(-1) esperará no solo a que se waitpid(-1) inmediatos, sino también a cualquiera de los procesos depurados.

Señales


(Contenido adicional de un traductor: esta información no está en el artículo en inglés)
Como ya se mencionó al principio, la comunicación entre el programa depurado y el depurador se produce mediante señales. Un proceso recibe SIGSTOP cuando se conecta un depurador, y luego SIGTRAP cada vez que sucede algo interesante en el proceso que se está depurando, por ejemplo, una llamada al sistema o la recepción de una señal externa. El depurador, a su vez, recibe SIGCHLD cada vez que uno de los procesos depurados (no necesariamente el hijo inmediato) se "congela" o "congela".

ptrace(PTRACE_SYSCALL) proceso depurado se realiza llamando a ptrace(PTRACE_SYSCALL) (antes de la primera señal o llamada del sistema) o ptrace(PTRACE_CONT) (antes de la primera señal). Cuando las señales SIGSTOP/SIGCONT también se utilizan para fines no relacionados con la depuración, pueden surgir problemas con ptrace : si el depurador "descongela" el proceso depurado que recibió SIGSTOP , entonces, desde el exterior, parecerá que la señal fue ignorada; si el depurador no "descongela" el proceso que se está depurando, entonces el SIGCONT externo no puede "descongelarlo".

Ahora para la parte divertida: Linux prohíbe que los procesos se depuren ellos mismos , pero no impide la creación de bucles cuando un padre y un hijo se depuran entre sí. En este caso, cuando uno de los procesos recibe una señal externa, se "congela" a través de SIGTRAP , luego SIGCHLD envía al segundo proceso y también se "congela" a través de SIGTRAP . Es imposible sacar a esos "co-depuradores" del punto muerto enviando SIGCONT desde el exterior; la única forma es matar ( SIGKILL ) al niño, luego el padre saldrá de la depuración y se "congelará". (Si matas a un padre, el niño morirá con él). Si el niño activa la opción PTRACE_O_EXITKILL , el padre depurado por él también morirá.

Ahora sabe cómo implementar un par de procesos que, al recibir cualquier señal, se congelan para siempre y mueren solo juntos. Por qué esto puede ser necesario en la práctica, no explicaré :-)

Source: https://habr.com/ru/post/439882/


All Articles