Limited Entropy Dot Com Not so random thoughts on security featured by Eloi Sanfèlix

28Oct/070

Trazando procesos con ptrace()

Esta vez vamos a hablar un poco de ptrace(), una llamada al sistema de los sistemas Unix (al menos la mayoría de ellos) que permite trazar procesos: ejecutarlos paso a paso, ver qué hay en su memoria, en los registros, etcétera. Aunque por ejemplo la implementación de ptrace() de OS X es bastante poco útil y sólo permite ejecutar paso a paso, continuar la ejecución y poco más... pero bueno, de eso hablaré un poco al final 😉

En este post voy a explicar cómo va ptrace y algunas de las posibilidades que ofrece en linux, aunque para eso ya está la página del manual pero siempre aportará algo. Finalmente enlazaré un código que he hecho como ejercicio de la asignatura Linux Kernel & Hackers Hut: se crea un hijo, se traza con ptrace() y se ejecuta un comando pasado como parámetro en el hijo, pero se modifican todas las llamadas a open("/etc/passwd",...) por open("/tmp/passwd",...). También añadí las llamadas a stat64() porque la utilidad cp tiene una pequeña protección: primero se llama a stat64 sobre el fichero origen, luego se abre el fichero, y se llama a fstat64() sobre el descriptor. Tras esto, se comprueba que las dos estructuras devueltas por stat64 y fstat64 sean del mismo fichero, mirando que el i-node y el identificador de dispositivo coincidan.

Prototipo de la llamada ptrace()

De la página del manual, vemos el siguiente prototipo:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

Empezando a trazar un proceso

Para trazar un proceso, tenemos dos formas. En primer lugar, con ptrace(PTRACE_ATTACH,pid,0,0) se puede empezar a trazar el proceso con PID pid. Sólo se permite trazar un proceso si se tiene el mismo uid o bien se es root.

Otra opción es crear un nuevo hijo con fork() o similares, y que el hijo llame a ptrace(PTRACE_TRACEME,0,0,0). A partir de entonces, el hijo es trazado por el padre.

Los parámetros que he puesto a 0 en este caso son ignorados, así que vale 0 o cualquier otra cosa que coincida con el tipo del parámetro ( yo siempre he visto/puesto NULL o 0 que viene a ser lo mismo 😉 ).

Vale, ya estoy trazando, y ahora qué?

Una vez estamos trazando el proceso, cualquier señal que reciba será notificada al proceso trazador (lo llamaré padre de ahora en adelante) vía wait() o waitpid(). Además, si el proceso hace una llamada a exec para cambiar la imagen/el ejecutable, también será parado.

En el caso de un fork()+exec() (típico) , lo que haremos será un waitpid(), esperando a que se pare tras hacer el exec. A partir de ahí podemos empezar a usar ptrace() :-). Si no hacemos el waitpid() y empezamos a usar ptrace, cabe la posibilidad de que el hijo aun no haya hecho el PTRACE_TRACEME, y obtendremos errores.

Una vez hecho esto, nuestro proceso estará parado y hay que elegir qué hacer con él. Tenemos varias opciones, dependiendo del parámetro REQUEST que pasemos. Describo aquí las más usuales:

  • PTRACE_CONT: Hace que el proceso con identificador pid continue hasta nueva orden (recepción de una señal por ejemplo). addr se ignora y data (si es distinto de 0) indica una señal que se le pasará al hijo cuando inicie su ejecución.
  • PTRACE_SYSCALL: Exactamente igual que PTRACE_CONT, pero hasta el inicio o salida de una llamada al sistema.
  • PTRACE_GETREGS / PTRACE_SETREGS: Leer/escribir los registros del procesador. Se pasa un puntero a una estructura de tipo user_regs_struct en el parámetro data.
  • PTRACE_POKETEXT/PTRACE_POKEDATA: Permite escribir en el espacio de instrucciones/datos del proceso, en la dirección indicada por addr el valor indicado por data.
  • PTRACE_PEEKTEXT/PTRACE_PEEKDATA: Como el anterior, pero leyendo de la dirección addr y devolviendo el valor leído. Hay que tener cuidado pues aquí -1 es un valor válido, y para saber si la llamada dio error hay que poner errno=0 antes de llamarla, y comprobar que siga siendo 0 después.
  • PTRACE_KILL: Manda un SIGKILL al hijo para matarlo.

Hay unas cuantas posibilidades más, aunque con estas ya se puede jugar bastante. Si alguien quiere saber más, la página del manual lo explica suficientemente.

Hay que tener cuidado de no llamar a ptrace() con algo distinto a PTRACE_KILL si el proceso hijo no está parado (es decir, hay que llamar a ptrace tras un wait() ) o dará error.

Finalmente, se puede dejar de trazar un proceso con el parámetro request a PTRACE_DETACH. De esta forma el proceso trazado sigue su curso normal y el trazador puede terminar o hacer cualquier otra cosa.

Ejemplo de uso: xtrace

Aquí podéis ver el código de una aplicación que hace uso de ptrace() para sustituir los open("/etc/passwd",...) por open("/tmp/passwd",...). Al principio en lugar de usar el wait() que comento para esperar a que el hijo haya llamado a PTRACE_TRACEME sincronizo padre e hijo mediante una señal, aunque es más sencillo lo otro. Lo hago así porque me dio problemas y no sabía que con el wait() valía, y un ejemplo de mi profesor utilizaba este mecanismo.

El código no está muy comentado, pero con lo explicado aquí creo que será suficiente para entenderlo.  De todas formas, si alguien trata de entenderlo y necesita ayuda ahí tiene los comentarios o mi correo 🙂

Posted by Eloi Sanfèlix

Filed under: GNU/Linux Leave a comment
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.