Inyección de memoria en Linux I

Normalmente estoy haciendo varias cosas a la vez, y relacionado con una de ellas estuve probando formas de inyectarme dentro de la memoria de otro proceso y forzarlo a ejecutar el código que yo quiera. Aunque tenemos varias cosas a medias en el blog (parte de la serie exploiting y otras cosas), he decidido contar un poco este tema, que aunque es antiguo y muy conocido, no deja de ser interesante. Nota: todo el código mostrado a continuación es para plataformas de 32 bits, para adaptarlo a 64 bits sería necesario modificar los accesos a regs.eip por regs.rip y el ensamblador ligeramente.

Vamos con ello entonces. En esta entrada vamos a servirnos de la API de depuración que ofrece el kernel para controlar otro proceso, y una vez hecho, le forzaremos a ejecutar nuestro código. En Linux, esta API es ptrace, que es usada por los depuradores como gdb para inspeccionar y modificar a un proceso. Básicamente lo que hace ptrace es convertirnos en el padre del proceso elegido, y pararlo para que podamos examinarlo. Como debería resultar evidente -esas mentes calenturientas que se detengan ya-, no es posible attachearse a un proceso que no nos pertenezca, salvo que seamos root, en cuyo caso sí. Esto quiere decir que si podemos realizar un PTRACE_ATTACH con éxito sobre un proceso dado podremos controlarlo. En esta primera parte, para no complicarnos, haremos un proceso víctima sobre el que inyectaremos código. Nuestro proceso hará lo siguiente:


#include <stdio.h>
#include <unistd.h>

int main(int argc, char** argv){
 int v = 5;

 while (1){
 printf("El sentido de la vida es %d\n", v);
 sleep(1);
 }
 return 0;
}

Se queda en un bucle infinito mostrando por pantalla, cada segundo, un mensaje (equivocado) acerca del sentido de la vida. Nuestra misión por tanto será hacer que diga la verdad y que se calle para siempre 😛

Qué inyectar y cómo hacerlo

Evidentemente, para inyectar código en ejecución, necesitamos que ese código esté en un lenguaje que el procesador sea capaz de comprender, algo similar a lo que sucede con una shellcode. Del mismo modo que con las shellcodes, necesitamos que el código sea independiente de la posición, ya que no podemos garantizar dónde se inyectará. En nuestro caso, queremos que la víctima muestre un mensaje y salga, así que nuestro código podría ser algo como lo siguiente:


BITS 32

 xor eax, eax
 ;ssize_t write(int fd, const void *buf, size_t num);
 mov al, 4    ;write es la syscall 4
 xor ebx, ebx ; limpiamos ebx, por lo que pudiera contener en el momento de la inyección
 inc ebx          ; 1 es el descriptor de salida estándar
 jmp short data ; saltamos para poder hacer call hacia arriba y evitar null bytes
back:
 xor ecx, ecx  ; limpiamos por si acaso
 mov ecx, [esp] ; colocamos la dirección de la cadena
 xor edx, edx     ; limpiamos por si acaso
 mov dl, 28        ; colocamos la longitud de la cadena
 int 0x80           ; llamada al sistema operativo

 ;void _exit(int status);
 xor eax, eax     ; limpiamos
 inc eax               ; exit es la syscall 1
 xor    ebx, ebx  ; exit status = 0
 int 0x80

data:
 call     back ; metemos en la pila la dirección de la cadena
 db    "El sentido de la vida es 42",0x0a   ; Este sí que es el sentido de la vida

Para los que este código os suene a chino, echadle un ojo a las entradas sobre shellcodes (I, II) y lo entenderéis. Aunque no es necesario eliminar los null bytes para este propósito, me he tomado la molestia de hacerlos, ya que así podré utilizar strlen sobre la cadena resultante. En el programa inyector, colocaremos este código tras ensamblarlo con nasm, igual que hicimos en los exploits con las shellcodes. Es importante limpiar los registros antes de utilizarlos, sobretodo si se van a realizar operaciones sobre las partes altas o bajas (al, dl…) ya que no afectan al contenido del resto del registro y puede quedar “basura” que estuviera usando la víctima antes de la inyección. Para ensamblarlo bastará con lo siguiente:


adrian@Andromeda:~/projs/injection$ nasm icode.S

Y podemos ver el contenido en código máquina (que colocaremos en el programa inyector), con hexdump.


adrian@Andromeda:~/projs/injection$ hexdump -C icode
00000000  31 c0 b0 04 31 db 43 eb  12 31 c9 8b 0c 24 31 d2  |1...1.C..1...$1.|
00000010  b2 1c cd 80 31 c0 40 31  db cd 80 e8 e9 ff ff ff  |....1.@1........|
00000020  45 6c 20 73 65 6e 74 69  64 6f 20 64 65 20 6c 61  |El sentido de la|
00000030  20 76 69 64 61 20 65 73  20 34 32 0a              | vida es 42.|

Ahora que ya tenemos listo lo que queremos inyectar y lo hemos preparado de una forma que funcione correctamente, vamos a ver el programa que hará el trabajo.

Inyectando

A continuación se muestra el programa (una POC, para hacerlo fiable y eficiente habría que añadirle algunas cosas). Leedlo por encima, y ahora comentaremos las partes más importantes.


#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <string.h>
#include <sys/user.h>
#include <signal.h>
#include <errno.h>

// Código máquina a inyectar, ensamblado con nasm desde el ASM que vimos antes
unsigned char code[]=
 "\x31\xc0\xb0\x04\x31\xdb\x43\xeb\x12\x31\xc9\x8b\x0c\x24\x31\xd2"
 "\xb2\x1c\xcd\x80\x31\xc0\x40\x31\xdb\xcd\x80\xe8\xe9\xff\xff\xff"
 "\x45\x6c\x20\x73\x65\x6e\x74\x69\x64\x6f\x20\x64\x65\x20\x6c\x61"
 "\x20\x76\x69\x64\x61\x20\x65\x73\x20\x34\x32\x0a";

int main(int argc, char** argv){
 pid_t pid;
 int i;
 struct user_regs_struct regs;

 if (argc != 2){
 printf("Usage: %s <process id>\n",argv[0]);
 exit(1);
 }

 pid = (pid_t)atoi(argv[1]);

 printf("[?] Attaching to process %d...\n",pid);
 if (ptrace(PTRACE_ATTACH, pid,0,0) < 0){
 perror("[!] ptrace: ATTACH");
 exit(1);
 }
 printf("[+] Succesfully attached!\n");

// Esperamos para asegurarnos de que el proceso se ha parado
 wait(NULL);

 printf("[?] Getting register information for the process...\n");
 if (ptrace(PTRACE_GETREGS, pid, 0, &regs) < 0){
 perror("[!] ptrace: GETREGS");
 exit(1);
 }
 printf("[+] Got eip pointing at 0x%x\n", (unsigned int)regs.eip);

 printf("[?] Injecting code at eip...\n");
 for (i=0;i<strlen(code);i++){
 if (ptrace(PTRACE_POKEDATA, pid, regs.eip+i,*(unsigned char*)(code+i))) {
 perror("[!]ptrace: POKEDATA");
 exit(1);
 }
 }

 printf("[+] Code injected succesfully!\n[?] Detaching...\n");
 if (ptrace(PTRACE_DETACH, pid, 0, SIGCONT) == -1){
 if (errno != ESRCH){
 // no debería suceder
 perror("[!] Detach: ");
 exit(1);

 }
 // deberia comprobarse que el proceso existe,
 // si existe, debería pararse con SIGSTOP antes del
 // detach. Si está parado por otra señal, hacerlo continuar
 // y pararlo... y teniendo en cuenta threads y grupos...
 // pero esto es una POC 😛
 kill(pid, SIGCONT);
 }
 printf("[+] Done!\n");
 return 0;
}

En primer lugar, perdonadme por el spanglish. Los comentarios los he puesto en castellano para que se entienda mejor el código, si bien los mensajes los estaba poniendo en inglés por si hacía una versión mejorada del programa, que pudiera usarlo más gente. Explicamos el asunto. La variable code, contiene el programa que vimos antes en ensamblador, pero ya pasado por nasm, es decir, en código máquina, de tal forma que pueda ejecutarse directamente donde lo copiemos. La estructura user_regs_struct está definida en <sys/user.h> y contiene los registros del procesador. La usaremos para leer del proceso víctima el EIP, e inyectar allí nuestro código.

Lo primero que hacemos es engancharnos al proceso cuyo pid hemos recibido como parámetro, mediante la orden ptrace(PTRACE_ATTACH, pid, 0, 0). Al realizar un ATTACH, los dos últimos parámetros de ptrace (data, y addr) son ignorados, por lo que los pondremos a cero. Esto nos convierte en el padre del proceso pid, y lo detiene. Sin embargo, es posible que al terminar la llamada a PTRACE_ATTACH, el proceso aún no se haya detenido, por lo que nos aseguramos de ello con un wait. Una vez que el proceso se ha detenido, leemos sus registros del procesador con ptrace(PTRACE_GETREGS, pid, 0, &regs) y los almacenamos en regs. De ahí extraeremos el EIP, y en el bucle siguiente, escribiremos nuestro código donde apunta EIP (para que se ejecute en cuanto el proceso continúe), con ptrace(PTRACE_POKEDATA, pid, regs.eip+i,*(unsigned char*)(code+i)). POKEDATA escribe en el espacio de memoria del proceso pid, en la dirección (regs.eip+i), el valor de (code+i).

Si todo ha ido bien, lo siguiente que deberíamos hacer es desengancharnos del proceso y que continúe su ejecución. Para ello, usaremos ptrace(PTRACE_DETACH, pid, 0, SIGCONT). Esto nos desenganchará y hará que el proceso continúe desde donde fue detenido (donde hemos inyectado nuestro código). En un entorno “real”, serían necesarias muchas más consideraciones a la hora de desengancharse del proceso. Por ejemplo, si el proceso está en un grupo (es un thread), el tratamiento de las señales es un poco más esquivo, y hay que hacerlo con más cuidado. Del mismo modo, Linux fuerza a que el proceso esté detenido por SIGSTOP para poder desengancharte, por lo que debe comprobarse si esto es así, y de no serlo, hacer que continúe hasta que se pare por SIGSTOP, y entonces desengancharte. Existen otros pequeños detalles a tener en cuenta, pero no son demasiado relevantes para esta pequeña introducción.

El resultado de todo esto es el siguiente.

Inyección de memoria

Enseñandole el verdadero sentido de la vida

Muy bien, ¿y todo esto para qué sirve? Pues aparte de para programar un depurador (gdb, strace…) para cosas más interesantes, que veremos en la siguiente entrada 😉 Hasta entonces, cualquier aportación, tenéis los comentarios.

Anuncios
Tagged with: , , , , ,
Publicado en hacking, Linux, malware, Programación
4 comments on “Inyección de memoria en Linux I
  1. Mensa13 dice:

    Muy buen artículo. Muy clarito y ameno. El uso que yo le había dado a ptrace era para un depurador, nunca se me había ocurrido inyectar código en .text xDDD

    Por cierto, es posible usar esta técnica en un programa seuidado para hacer que ejecute el shellcode aunque no seas el propietario?

    Un saludo.

  2. Adrián dice:

    Hola Mensa13,

    no sé si entiendo bien tu pregunta. Así que voy a desglosar los dos casos (aunque la respuesta es la misma).

    Si el binario pertenece a root y tiene el bit setuid, cuando lo lances con tu usuario no prvilegiado, el usuario efectivo de ese proceso será root. Si intentas hacer ptrace a ese proceso, desde tu usuario, obtendrás un mensaje “Operation not permitted”.

    Si el binario pertenece a tu usuario no privilegiado y tiene el bit setuid, cuando lo lances como root, el usuario efectivo será tu usuario, pero el usuario real será root. Si intentas hacer ptrace a ese proceso desde tu usuario, tendrás el mismo mensaje que antes “Operation not permitted”.

    Si no se hicieran estos controles, gracias a la familia de funciones setreuid podrías inyectar código de usuario que ejecutaría root 😉

  3. Mensa13 dice:

    Gracias esa era la pregunta. ( Una pena que no se pueda 😛 )

  4. oPen sylar dice:

    Vaya.. Muy interesante el post… Al igual que el blog.. Son pocos los que ofrecen información sobre Linux e inyecciones.. Agregado a mis bookmarks.. Saludos

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: