Black Hole organizacion
CostaRica  
  Bienvenida
  Hackers
  Foro ingresa
  Noticias
  Contacto
  Imagenes
  Programas
  Visitantes
  Chat
  Libros
  => hacker
  => Virus
  => Cracker
  => Lecciones hackin
  => Programacion
  => Diseños web
  => Guia de hackin
  => Privasidad
  => Guia → 1
  => Guia → 2
  => Guia → 3
  => Guia → 4
  => Guia → 5
  => Guia → 6
  => Guia → 7
  => Guia → 8
  => Guia → 9
  => Guia → 10
  => Guia → 11
  => Como intrudusirse aun sistema
  => shellcodes_linux-1
  => shellcodes_linux -2
  => UN-scodes.
  => Guia version Deluxe
  => Ser Hacker dentro de Términos Legales
  => Como hackear una paginaweb
  => Comoprogramar un Virus
  => Como Crear un Virus
  => Cuantos tipos de Virus existen
  => Programaciones de un virus
  => Estructura de computadores
  => Fundamentos de SSOO
  => Sistemas de numeración
  => Ensamblador I: Conceptos básicos
  => Ensamblador II
  => Utilidades para la programación
  => Infección bajo Windows
  => Infección bajo Linux
  => Técnicas avanzadas
  => Apéndices
  => CONOCIENDO LA MAQUINA
  => DIRECCIONAMIENTO DE MEMORIA EN EL 8086
  => CHIPS DE APOYO (Ampliación de la lección 1)
  => LA PILA DEL 8086
  => CODIFICACIÓN DE LAS INSTRUCCIONES EN EL 8086
  => Manual HTML
  => Ataques basados en Desbordamiento de Buffer (Buffer Overflow)
  => Privasidad
  => Escaneo
  => anti Escaneo y Escaneo
  => Malianom
  Triang
  Tersirve esta paguina?
  -
  juegos
  Vagos
  mapa
  Mapa del sitio
  546
Infección bajo Linux

Atras


 

8.0.0.- Disclaimer y demás

No se iba a librar el sistema operativo Linux de que le metamos nuestras zarpas, ¿verdad?. Es curioso, porque mucha gente tiene esa opinión de "¡Ja! ¡Linux jamás puede ser infectado, no te esfuerces!", frase tras la cual vuelven a sentirse seguros en sus Linux sin preocuparse siquiera por averiguar si esto es cierto. Lamentablemente este es el tipo de actitud que le lleva a uno a confiarse y no ver los agujeros de seguridad que pueden traer problemas. Nunca hay que decir "jamás", siempre alguien encontrará una manera; y la actitud correcta consiste en localizar esos agujeros y taparlos para construir un sistema operativo más robusto y fiable, o algún día el usuario de Linux se encontrará con una infección a la que no sabrá hacer frente. Tapar agujeros de seguridad es algo que no podemos hacer con Windows, pues solo ellos pueden repasar todos los boquetes que tiene; y no lo va a hacer porque se lo digamos. Sin embargo Linux es un sistema operativo que todos pueden ayudar a construir, con lo que programar virus que utilicen posibles huecos de seguridad puede convertirse en una tarea loable al advertir de problemas que puedan existir en este sistema.

Tampoco nos engañemos; por suerte no es sencilla una infección masiva en Linux, puesto que los sistemas de privilegios en acceso a ficheros y demás unido a la costumbre de compilar uno mismo el código, evitan de forma razonablemente buena esta posibilidad. Los usuarios de Linux no se envían ejecutables attacheados en emails cuyo texto diga cosas como "enanito si, pero con unos coj...", email ante el cual picaron unos cuantos usuarios de Windows cuando salió el Hybris de Vecna. Pero precisamente creo que el hecho de esta dificultad es lo que llama la atención en Linux e impulsa a uno a intentar atacarlo, ¿cómo programar virus para un sistema operativo supuestamente tan seguro e inexpugnable? Pues bien, más o menos es acerca de lo que vamos a hablar en esta entrega del curso de programación de virus.

Un último detalle; como dije en la entrega sobre infección en Win32, la programación de virus es algo de por sí interesante y que no necesita de que putees a nadie con un virus que hayas programado. El virus se reproduce igual en tu disco duro que en el de otros, y si lo que te interesa es ese rango de cosas que engloba la vida artificial, analizar un sistema operativo para sacar sus agujeritos o buscar fallos de seguridad... ¿qué diferencia hay si no lo sueltas?. Distribuir un virus no tiene ningún sentido, sólo conseguirás joder a pobres usuarios que, aunque tu virus no tenga código destructivo, temerán que si lo tenga... y que puede que en respuesta formateen su disco duro o algo peor, que equivaldría a que tu código tuviera código destructivo.

No me puedo hacer responsable de lo que hagáis con esta información, pero por lo menos os doy un poquito la chapa , aunque, como digo, dudo que alguien a quien realmente le interesa el tema y está haciendo un esfuerzo importante para aprender e investigar, le quede tiempo para la estupidez de soltar el virus... el placer, está en programarlos.

 

 

 

8.1.- Introducción a Linux

 

8.1.1.- Estrategias de aproximación a Linux

Lo primero que vamos a tener en cuenta es que Linux es sin duda un sistema operativo mucho mejor protegido que Windows. Vamos a estar limitados a los permisos del usuario que ejecute el fichero infectado, y tendremos que actuar en consecuencia. Una de las formas de plantear virus para Linux será la de conseguir estos privilegios de root, con lo que podremos hacer lo que nos venga en gana. No es tan fácil, de todos modos, y podemos citar las siguientes estrategias:

- Solución: Algoritmo del avestruz. Consistente en no hacer nada. Es decir, nosotros infectamos revisando los permisos de los ficheros, o ni siquiera los revisamos y cuando la escritura falla actuamos en consecuencia no infectando el fichero. Esta parece en principio la peor solución, dado que la acción reproductora del virus está muy limitada, aunque tampoco se puede decir que sea una mala solución; la esperanza estaría entonces en que sea ejecutado por el superusuario y entonces aprovechar para instalarse en todos los lugares posibles del sistema, especialmente en sbin/init :-)

- Solución: Uso de exploits. Consistiría en buscar alguna falla del sistema para poder hacerse superusuario y poder infectar sin problemas. El defecto de esta solución es evidente; Linux se revisa constantemente, y los nuevos kernels o versiones del programa afectado llevarían ese fallo parcheado, con lo que el virus perdería su efectividad. Se trata, más bien, de una solución para sistemas Windows

- Solución: Windows/VMWare. Dado que Linux está tan protegido, se puede uno aprovechar del hecho de que la mayor parte de la gente que tiene Linux instalado utiliza también Windows (aunque le cueste admitirlo xD). La cuestión es que un infector multiplataforma para Windows y Linux podría leer diréctamente la tabla de particiones del disco o discos duros, localizar la partición Linux y a través de Windows, momento en que el sistema de ficheros de Linux no está protegido, infectar leyendo las propias tablas de i-nodes los ficheros ELF, de cabeza a por el sbin/init. Con este método, aunque es un tanto difícil y pesado de llevar a cabo (manejar a mano los ficheros con nuestras propias funciones puede ser un tanto tedioso), tenemos la inmensa ventaja de que infectando algún proceso importante podríamos posteriormente meternos donde nos de la gana, y que se podrían realizar ataques a través de aplicaciones tan populares como VmWare (que puede ser fácilmente detectado dado que para su funcionamiento utiliza un ring que no es ni 0 ni 3 en la máquina virtual, lo cual se puede detectar en la terminación de los descriptores de segmento).

- Solución: Infección de RPMs. Una de esas cosas sensibles y con escasa seguridad para un ataque de virus son los ficheros de Redhat Packet Manager o RPM; cada vez es más usual distribuir programas en este formato. Pero este formato tiene unas características muy particulares, que unidas al hecho de que suele ejecutarse como root su instalación, lo muestran como otra forma de acceder a Linux a través de virus informáticos.

 

 

8.1.2.- Atacar al SO Linux

En este punto encontramos pocas salidas; siempre puede encontrarse un exploit en el kernel de Linux que nos dé privilegios de ring0 en el procesador con lo que podamos hacer lo que queremos, pero ese exploit sabemos que será corregido, con lo que el virus perderá en poco tiempo su funcionalidad. El sistema de protección de memoria bajo Linux está muy bien desarrollado, y no hay forma en circunstancias normales de salir del modo usuario de ejecución (ring3) para hacer en superusuario lo que nos venga en gana. En Windows sí se hace, pero también es cierto que nadie corrige bugs en Windows...

Bajo Linux, tenemos una división de memoria que sitúa 3/4 partes de las direcciones virtuales de memoria (00000000h a C0000000h) para procesos de usuario, y 1/4 para el kernel (0C0000000h a 0FFFFFFFFh) El anillo de ejecución del procesador (hay dos, el ring0 o superusuario y ring3 o usuario) se ve fácilmente en el descriptor de segmento al que se refiere la parte a la que se intenta acceder. Sus dos últimos bits indican el RPL o modo de ejecución, estando los dos activados para ring3 y ninguno para ring0. Bajo Linux precisamente se inicializan cuatro segmentos básicos, para código y datos en kernel y procesos de usuario; 010h y 018h son código y datos del kernel respectivamente, y 23h y 2bh para código y datos de procesos de usuario.

010h -> Kernel -> 00010000b

018h -> Kernel-> 00011000b

023h -> Usuario -> 00100011b

02bh -> Usuario -> 00101011b

La pregunta entonces, dado que no podemos acceder a la zona reservada a kernel más allá de la dirección de memoria C0000000h, es, ¿cómo entonces podemos acceder a funciones de manejo de disco, etc, si normalmente estamos en ring3?. Para eso se implementan las interrupciones, y en particular la básica de la API de Linux, la int 080h (también hay otro sistema equivalente para compatibilidad con otros Unix como Solaris que utiliza Lcalls, aunque no tenemos nada que hacer aquí)

Al llamar a la int 080h, el procesador consulta una tabla de vectores de interrupción, saltando a la dirección indicada para esta interrupción y pasando automáticamente a ring 0. Por lo tanto podemos hacer esta llamada, pero no podemos modificar ni la indicación del lugar al que salta, ni aquello que hay dónde salta; así pues lo que nos proporcionará la int 080h es la API básica del sistema operativo, que puede ir desde la modificación de ficheros al manejo de sockets. De hecho, todo el tiempo cuando programemos virus, utilizaremos esta función 080h para utilizar la API de Linux.

Como conclusión entonces, la que ofrecí antes; que la única forma de atacar esto es buscar algún exploit, lo cual no es un trabajo sencillo y que tiene el gran inconveniente de que va a servir de poco cuando se saque un parche.

 

 

8.1.3.- Utilizando la API: Buscando archivos

La forma de llamar a la API del sistema y en particular la que vamos a utilizar, va a ser muy parecida a lo que hacíamos en Ms-Dos. Vamos a poner en AL el valor de la función a la que queremos llamar, y en el resto de registros (ordenados como eax-ebx-ecx-edx-etc) diversos parámetros de nuestra llamada a función. Veamos una mini-lista de funciones que se pueden utilizar con la int80h (ojo, hay muuuuuchas más, esto es solo orientativo; se pueden encontrar listas completas en linuxassembly.org):

1 sys_exit Salir del programa en ejecución
2 sys_fork Hacer un "fork" del proceso (esto significa, "dividirlo" en dos procesos y programar de modo que cada uno siga una línea de ejecución)
3 sys_read Lectura de un fichero/dispositivo
4 sys_write Escritura en un fichero/dispositivo
11 sys_execve Ejecución de un programa
21 sys_mount Montar un sistema de ficheros
... sys_... ...

 

Ahora veamos la primera función que nos va a interesar; es la función Open. Tras haber sacado el Delta Offset como hacíamos en Windows (eso es algo que traspasa fronteras y sistemas operativos xD), podríamos hacer algo como esto:

mov eax,05h lea ebx,[diractual+ebp] xor ecx,ecx xor edx,edx int 080h  <codigo>  diractual: db	'.',0 

Vale, ¿qué significa esto? Pues nada más y nada menos que una llamada a "Open", que realizamos sobre el directorio actual (el '.' en diractual). En Linux la forma de hacer el FindFirst/FindNext que hacíamos en Windows es bastante diferente a como lo hacíamos en Windows; tendremos que abrir el directorio para luego ir leyendo sus contenidos con la API ReadDir. Curiosamente esta es de las partes más difíciles cuando uno se pone desde a cero para escribir un virus en Linux, pues veremos que aunque Linux esté muy documentado, en algunos casos la documentación no es correcta (y que nadie se asuste si digo que más de una vez al hacer cosas no habrá más remedio que leerse los fuentes del kernel para ver cómo se hace).

El caso es que lo siguiente que tenemos que hacer es llamar a la órden ReadDir, que nos va a leer una entrada de ese directorio que acabamos de abrir. Una forma de hacerlo es lo siguiente:

mov eax, 059h ; readdir lea ecx, [buffer + ebp] int 080h or ax,ax jz fallo

El parámetro en ECX va a apuntar a un buffer de un tamaño 10Ah, que es la estructura de fichero que nos va a devolver esta llamada. Así, en esta estructura tendremos diversos datos sobre el ejecutable (veamos lo que nos dice sobre ella dirent.h en el kernel):

struct dirent {  long d_ino;  off_t d_off;  unsigned short d_reclen;  char d_name[256]; /* We must not include limits.h! */ };

Lo más importante es lo que tenemos en el desplazamiento 0Ah respecto al principio de la estructura; el nombre del fichero (el primer valor es un long, 4 bytes, el segundo es un puntero, 4 bytes, el tercero es un short, 2 bytes). A partir de él, podremos abrirlo, mapearlo en memoria y finalmente realizar nuestro objetivo; infectarlo. El resto de valores son el i-node (d_ino) correspondiente, el offset respecto a la entrada de directorio (d_off) y el tamaño del nombre (d_reclen) presente en d_name.

 

 

8.1.4.- Apertura y proyección en memoria

Tal y como hicimos en Windows, en Linux vamos a aprovechar el hecho de que se nos permite mapear ficheros en memoria. Es decir, que en lugar de utilizar un puntero para leer y escribir sobre él, podemos proyectarlo sobre una zona de memoria y escribir sobre ella como si lo hiciéramos en el fichero. Después, al cerrarlo, los cambios que hayamos realizado se guardarán.

mov eax, 5 lea ebx, [buffer + 0Ah + ebp] mov ecx, 2 xor edx, edx int 080h

Parte de esto es muy comprensible; sí, 05h es una función que ya conocemos, la de apertura del fichero. Ebx sin duda está apuntando al nombre del fichero, necesario para abrirlo, mientras que ecx tiene como valor un "2". ¿Qué significa esto? Bien, significa que queremos acceder en lectura/escritura. No hará falta comprobar si tenemos acceso al fichero, si no tenemos permiso la llamada fallará y buscaremos otro. El parámetro EDX en esta llamada hace referencia a un "modo" en caso de que el fichero no exista y lo estemos creando, pero sin duda este no es el caso.

La cosa es que esto nos ha devuelto un "handler" referente al fichero, y nos lo ha devuelto en EAX. Con este descriptor vamos a seguir actuando, y el caso es que lo siguiente que querremos hacer será aumentar el tamaño del fichero para que se adapte a nuestros deseos; si lo vamos a mapear en memoria, querremos poder acceder a él complétamente.

Lo siguiente que hagamos al abrir un fichero entonces, será averiguar cual va a ser su longitud, y para esto tenemos una llamada a la función Lseek, la cual tiene como número de función el 13h. Teniendo tras el anterior código el handler o descriptor en eax, hacemos lo siguiente:

mov ebx, eax mov eax, 013h mov ecx, 0h mov edx, 2h int 080h

El tamaño que pongamos en ECX es lo realmente importante, pero va a ir respecto a EDX. ¿Pero en qué sentido? Pues bien, en EDX vamos a indicar una de tres posibilidades, SEEK_SET, SEEK_CUR y SEEK_END (valores decimales 0, 1 y 2). Con la primera opción, ECX indica el número de bytes del fichero de forma absoluta. Con SEEK_CUR, se hace respecto a la posición del puntero de búsqueda más ECX bytes... y por fin, con SEEK_END, será el tamaño del archivo más ECX bytes... así, en esta ocasión vamos a tener 0 en ECX y 2 en EDX, para averiguar el tamaño del fichero (en EAX) y usarlo posteriormente... ojo, que estamos hablando de un virus que infecta "cavity" (luego veremos qué significa), con lo que el tamaño del fichero no va a aumentar.

Bien, ya tenemos el fichero abierto y sabemos su tamaño, ¿qué es lo siguiente? Pues a no ser que seais masocas y querais jugar con punteros, lo mejor es mapear el fichero en memoria. Y para ello vamos a tener que llamar a la syscall mmap, encargada de ello. Si vemos una descripción, es la siguiente:

 
void *start (dword, preferred memory address or, NULL) size_t length(dword, file size) int prot (dword, PROT_READ/WRITE/EXEC) int flags (dword, MAP_SHARED/PRIVATE/FIXED) int fd (dword, Linux file descriptor) off_t offset

Y lo que diremos aquí será... otiaaaaaaaa que de parámetros, ¿no? ¿Y eso me cabe en los registros? Pues no... pero es que en Linux hay dos formas de llamar a la API del sistema a través de la int80h, dependiendo de la cantidad de parámetros que haya que meterle (suena algo burro pero es así). El caso es que cuando suceda como en este caso, Linux va a suponer que en EBX ponemos un puntero a todos esos parámetros, y los va a sacar de ahí. Lo más sencillo como podéis suponer, es meter esos parámetros en la pila y luego hacer un mov ebx,esp antes de llamar a la función, de manera que no tenemos que gastar espacio en nuestro programa ni nada por el estilo (lo cierto es que utilizar la pila para guardar datos es una maravillosa costumbre que optimiza mucho xD, por algo es lo que siempre se usa para las variables locales en funciones, pero eso es otra historia).

Veamos un ejemplo práctico de cómo hacerlo:
push 0 push ebx ; el handler o file descriptor push 1 ; privado push 3 push eax ; lo que nos devolvió la llamada a la int anterior push 0 ; NULL para que nos indique la dirección donde lo mapea mov ebx, esp mov eax, 0x5a int 080h cmp eax,0xFFFFF000 ; La comprobación de error que hace mmap.c jbe Continuar 

Con esto, deberíamos de haber abierto el fichero mapeado en memoria, y en EAX tendríamos la dirección base a partir de la cual ha sido mapeado. La comprobación de error que hago (cmp eax, 0xFFFFFF000h / jbe Continar) es así en el código de mmap.c (la syscall está mal documentada y da problemas a veces al comprobar si existe algún fallo si se sigue el procedimiento "standard").

Vistas estas cosas acerca de la API que vamos a tener que utilizar para infectar (por suerte no vamos a tener que hacer cosas tan terribles como hacíamos en Windows para tener que sacar las direcciones de la API al basarse en llamadas a la int80h), ya podemos empezar a hablar de ejecutables ELF en Linux.

 

 

 

8.2.- Infección de ficheros ELF

 

8.2.1.- Ficheros en Linux

En Linux básicamente tenemos los siguientes objetivos posibles (aunque como siempre, el límite a lo que podemos infectar lo pone nuestra imaginación):

- El formato a.out (casi no utilizado ya) es extremadamente vulnerable, puesto que su cabecera sólo indica el punto de comienzo de la ejecución y los tamaños y situación de las secciones. Infectar un fichero a.out es casi tan sencillo como con un COM de Ms-Dos, no consistiría más que en aumentar el tamaño de la sección de código, escribir el virus en ese tamaño que se ha aumentado y cambiar el puntero de comienzo de ejecución para que apunte al virus.

- Los ficheros RPM, el standard RedHat que algunas distribuciones importantes (RedHat, SuSe) usan para instalar paquetes, también son un bocado delicioso: en resumen no son más que archivos que contienen una serie de archivos comprimidos con gzip, sólo que con algo por lo que en dos y windows muchos escritores habrian dado un brazo. Esto son los "triggers", que son eventos que suceden cuando uno instala a ciegas su paquete rpm; y estos "triggers" consisten en shell scripts, con lo que suponiendo que un paquete infectado así se instale como root, para qué decir más...

- Otro punto interesante en Linux es, por supuesto, infectar código fuente; el C permite ensamblador in-line con lo que los sources de linux se convierten en un objetivo delicioso y difícil de descubrir, aunque tiene un alto riesgo de ser descubierto por cualquiera con ciertos conocimientos de C, al menos para saber que "eso no debería estar ahí".

- Finalmente, está el formato ELF; este es el formato ejecutable standard bajo Linux, y será nuestro objetivo principal; por ello, entramos más en detalle sobre él.

 

 

8.2.2.- Carga en memoria en Linux

El formato ELF es curiosamente mucho más sencillo que infectar que el PE de Windows, y nos permite una interesante variedad de métodos. Explicaré de modo sencillo como afecta la estructura a la ejecución:

Cuando un fichero ELF es ejecutado, es decir, cuando escribimos su nombre en el shell, suceden una serie de cosas; primero, el shell llama a la función execve() de las libc, la libc llama al kernel con sys_execve(), el cual abre el archivo mediante do_execve(), busca el tipo de ejecutable con la función interna search_binary_handler(), carga las librerías en caso de ser ELF que este necesite mediante load_elf_binary(), crea el segmento de código para el programa y finalmente mediante una llamada a start_thread(), pasa a ejecutarse el código del programa.

Linux asigna permisos a las páginas de memoria del programa (la memoria está dividida en páginas de 4Kb que tiene asignados permisos de lectura, escritura y ejecución), dividiendo en varios segmentos el fichero; en un modelo sencillo podríamos decir que encontramos código datos y pila (y por ejemplo, el código tendrá permisos de lectura y ejecución, mientras que el código los tendrá de escritura y lectura, pero no de ejecución). Un modelo sería este:

 

 
Código
 
Datos inicializados
 
Datos sin inicializar
 
(espacio libre)
 
Pila
 
Entorno del programa (argumentos pasados, variables de entorno y nombre del fichero ejecutable)

 

Determinado esto, se sitúan en estos segmentos las secciones individuales; por ejemplo, la .text representa normalmente la de código, .data los datos inicializados, .bss datos sin inicializar, .stack la pila, además de otros que a veces son complétamente inútiles (como la .comment o la .notes). Curiosamente, también las secciones tendrán permisos individuales aunque pertenezcan a un segmento cuyos permisos ya han sido dados, pero Linux no hace ni caso y usará el indicado en el segmento al que pertenecen. Precisamente, esto va a facilitar mucho las cosas a la hora de infectar estos ficheros, puesto que podremos trabajar a nivel de segmento y olvidarnos de estar tan pendientes de las secciones por separado (que es lo que sucedía en Windows).

 

 

8.2.3.- Formato ELF, desde dentro

 

La estructura física de este tipo de fichero es la siguiente:

 

 
Cabecera ELF
 
Program Header Table (opcional)
 
Sección 1
 
...
 
Sección N
 
Tabla de secciones

 

Lo primero que nos vamos a encontrar es la cabecera ELF; en ella tenemos el identificativo ELF (los 4 primeros bytes, 07fh + 'ELF'), seguido de una serie de datos acerca del fichero, que incluyen cosas como el tipo de ejecutable según procesador, alineamiento de bytes, tipo de fichero (ejecutable, obj, etc), la máquina que corre el archivo, y toda una serie de valores descritos en la siguiente estructura:

#define EI_NIDENT 16 typedef struct {  unsigned char e_ident[EI_NIDENT];  Elf32_Half e_type;  Elf32_Half e_machine;  Elf32_Word e_version;  Elf32_Addr e_entry;  Elf32_Off e_phoff;  Elf32_Off e_shoff;  Elf32_Word e_flags;  Elf32_Half e_ehsize;  Elf32_Half e_phentsize;  Elf32_Half e_phnum;  Elf32_Half e_shentsize;  Elf32_Half e_shnum;  Elf32_Half e_shstrndx; } Elf32_Ehdr; 

Nos interesarán especialmente:

- e_entry: El puntero (RVA) a inicio de ejecución de un nuevo programa respecto a la localización en memoria (e_entry). Una RVA, que ya definimos en Windows, es un puntero relativo a la dirección base en que el programa se carga en memoria. Es decir, que si el programa se carga en 040000h y el Entry Point es 200h, el programa comenzará su ejecución en 040200h.

- e_phentsize, e_phnum, e_shentsize, e_shnum, e_phoff, e_shoff: Diversos campos de descripción sobre entradas en la PHT (Program Header Table) y la SH (Section Header o Cabecera/Tabla de Secciones).

La siguiente sección a comentar es entonces la Program Header Table, que contiene las descripciones de los segmentos. Indicará en una estructura por segmento, el tipo de segmento, una dirección virtual de comienzo en memoria, tamaño, permisos y algunos otros datos.

Para nosotros va a ser muy necesario tocar esta tabla de entradas; por ejemplo, podemos poner a la parte de código permisos de escritura, con lo que podremos tener al virus en un sólo bloque sin repartirlo en secciones de código y datos (lógico). En caso de que almacenemos el virus en una sección del segmento de datos, lo que haremos será modificar sus permisos de lectura/escritura añadiendo ejecución para que podamos correr el virus sobre ella.

También, al infectar, habrá que aumentar el tamaño de algún segmento, aquel en que queramos meter el virus. Por ejemplo, aumentar la de datos si nuestro virus se mete ahí de forma que deje espacio para que se cargue en memoria.

Una vez acabamos con esto, nos queda la tabla de secciones; cada sección tiene una serie de datos que incluyen tipo, lugar en el fichero, RVA, tamaño, etc. Lo cierto es que ni tan siquiera hace falta tocar esta tabla para infectar, excepto para saber donde comienza una sección por ejemplo (en caso de querer hacer un cavity). El hecho de que Linux conceda prioridad a los segmentos, da muchas facilidades (aunque no sería difícil; en Windows el sistema standard de infección consiste en aumentar una sección de tamaño y añadirse en ella... y los ejecutables de Linux y Windows son tremendamente parecidos - el PE se derivó del COFF de Unix).

Un posible algoritmo pues de infección sería el siguiente:

- Apertura del fichero y comprobación de si es un ELF y si es ejecutable

- Buscamos en la tabla de secciones al final del fichero la .note, identificada por un campo que define el tipo en la estructura.

- Comprobamos si hay espacio suficiente para el virus, y si lo hay se averigua el offset físico donde está esta sección y se copia ahí.

- Recalculamos el punto de inicio de ejecución del fichero para que apunte a nuestro virus y guardamos el antiguo.

- Aumentamos el tamaño del segmento de datos para que el virus se cargue en memoria.

 

 

8.2.4.- Ejemplo de infección de un ELF

Bien, nos ponemos físicamente en el momento en que acabamos de mapear el fichero en memoria; la forma de infección que enseñaré, de tipo "cavity", es la que utilicé al programar el Lotek. Mientras que bajo Windows vimos un tipo de infección en la que nos añadíamos al final del programa, en esta ocasión nos vamos a colar en un hueco ya existente de él, en particular en la sección .notes. En fin, es el tipo de infección que acabamos de explicar y desarrollar en cinco puntos, sólo que ahora la desarrollaremos un poco más:

- Apertura del fichero y comprobación de si es un ELF y si es ejecutable

Suponemos que ya ha sido realizada la apertura del fichero y tendremos en EAX su dirección base. Comprobar estas cosas en código podría ser algo como esto:

cmp dword[ebx],0x464C457F ; Cadena 'ELF'+07fh jnz noesELF cmp byte [ebx + 0x10],02h ; Es ejecutable? jnz noesELF

Visto esto, ya sabremos si es un fichero que podemos infectar o no (tampoco está de más poner la típica marca de infección en algún lugar).

 

- Buscamos en la tabla de secciones al final del fichero la .note, identificada por un campo que define el tipo en la estructura.

Para esta búsqueda podemos tomar en esta ocasión un atajo (aunque tenemos el offset de la tabla de secciones y podemos iterar a través de ellas). Nos interesa la .note, y resulta que esta entrada suele estar con muy alta probabilidad en el final del fichero menos 04Ch

mov eax,[tamanyo] ; El cual habiamos averiguado antes con la función 13h sub eax,04Ch ; Restamos esto add eax,<direccion base> ; Y le añadimos la base donde se ha mapeado

 

- Comprobamos si hay espacio suficiente para el virus, y si lo hay se averigua el offset físico donde está esta sección y se copia ahí.

Para ello hemos de tener en cuenta la estructura de la entrada de la tabla de secciones a la que acabamos de acceder:

typedef struct {  Elf32_Word sh_name;  Elf32_Word sh_type;  Elf32_Word	sh_flags;  Elf32_Addr sh_addr;  Elf32_Off sh_offset;  Elf32_Word sh_size;  Elf32_Word	sh_link;  Elf32_Word sh_info;  Elf32_Word sh_addralign;  Elf32_Word sh_entsize; } Elf32_Shdr; 

Con esto en nuestras manos, lo que nos va a interesar pues es sh_size que indica el tamaño de la sección. ¿Será suficiente para alojarnos? Lo comprobamos con algo como lo siguiente:

cmp word[eax+10h],tamanyo_virus jb NoHayEspacio

Si tenemos suficiente espacio para alojar el virus, cogeremos ese sh_offset como una RVA y copiaremos el código del virus ahí, sobreescribiendo lo que se encontrase en ese lugar (que en cualquier caso, no sirve para nada y no impide el buen funcionamiento del programa):

mov edi,[eax+0Ch] add edi,<direccion_base_mapeado> lea esi,[ebp+inicio_de_nuestro_virus] mov ecx,tamanyo_virus rep movsb ; Copiado!

 

- Recalculamos el punto de inicio de ejecución del fichero para que apunte a nuestro virus y guardamos el antiguo.

Para ello, vamos a introducir la forma de otra estructura, la Program Header Table, que contiene información acerca de los segmentos:

typedef struct {  Elf32_Word p_type;  Elf32_Off p_offset;  Elf32_Addr	p_vaddr;  Elf32_Addr p_paddr;  Elf32_Word p_filesz;  Elf32_Word p_memsz;  Elf32_Word	p_flags;  Elf32_Word p_align; } Elf32_Phdr; 

En ella, por ejemplo p_type indica el tipo de segmento, p_offset el comienzo en fichero de este segmento, p_vaddr dónde se situará en memoria, etcétera. Y son estos valores los que habremos de tener en cuenta a la hora de calcular nuestro nuevo entry point y deducir el lugar en que se hallará el antiguo. Para averiguar el nuevo entry point, cogeremos el offset sobre el que ibamos a copiar el virus sin sumarle la dirección base (es decir, el sh_offset de la tabla de secciones), le restaremos el contenido de p_offset (lo cual nos deja el desplazamiento respecto al inicio del segmento), y sumándole finalmente p_vaddr ajustaremos no respecto a la base que hemos cargado en memoria, sino al segmento cuando este sea cargado en memoria (es decir, respecto a su desplazamiento en cuanto que es dirección virtual):

mov edi,[eax+0ch] ; Offset de la seccion sub eax,[ebx+098h] ; Offset seccion - offset segmento add eax,dword[ebx+09Ch] ; Desplazamiento ajustado al segmento mov dword [ebx+18h],eax ; Colocamos el nuevo entry point 

Como se puede ver, me estoy refiriendo a desplazamientos fijos al tocar la PHT; estoy asumiendo que 98h es p_offset, o que 9Ch es p_vaddr. Esto se debe a que ya de antemano según esta forma de infección sé esos desplazamientos por el sencillo motivo de que el segmento que hay que modificar siempre es el mismo, esto es, segmento de datos. Por lo tanto, tendremos también que hacer otra modificación en p_flags para que nos permita ejecutar código:

mov byte [ebx+0ACh],7 ; PF_R+PF_W+PF_X  

- Aumentamos el tamaño del segmento de datos para que el virus se cargue en memoria.

Podemos recalcular el aumento de tamaño respecto a la nueva sección (notes) que se carga en memoria, aunque es un valor que no vamos a tener mucho problema en ajustar en lo que diríamos "hardcoding", por ejemplo:

mov eax,1000h add dword [ebx+0A4h],eax add dword [ebx+0A8h],eax 

Un detalle que hemos de tener en cuenta, es que el tamaño de página al cargarse el ELF en memoria suele ser fijo, según el standard SYSTEM V puesto a 4Kb (o potencia superior). Por supuesto, el hecho de que ejecutemos un fichero no significa que él entero vaya a ser puesto en memoria; puede que hayan partes del programa que no vayan a utilizarse, con lo que se situará la referencia en HD en la página, con lo que al acceder se generará un fallo de página y la página será llevada a memoria. En resumen, que las páginas no se cargarán en memoria hasta que sean referenciadas.

Por ello, también se encuentran las referencias a tamaño virtual (en memoria) redondeadas a esta cantidad. Supongamos un fichero ELF con tres segmentos (cabecera, código y datos):

Sección Offset en el fichero Tamaño en fichero (p_filesz) Tamaño en memoria (p_memsz)
Cabecera
0
100h
No
Código
100h
1986h
2000h
Datos
2086h
987h
1000h

Con estos datos ficticios encima es sencillo ver un poco "por donde van los tiros" a la hora de utilizar el alignment (que por cierto también se usa en Windows). En memoria, si las páginas son de 4Kb, sólo se podrán poner permisos distintos a cada bloque de 4Kb; por tanto, el tamaño de la página ha de determinar cómo están alineados los segmentos. Sería absurdo tener en la misma página código y datos, dado que ambos tienen permisos distintos. Por ello, el hecho de alinear con esta diversidad de tamaños, en fichero y en memoria, servirá para mantener de forma coherente el sistema de páginas y segmentos separados.

 

 

 

8.3.- Técnicas avanzadas en Linux

 

8.3.1.- GOT, PLT y la posibilidad de residencia per-process

Existen dos secciones, conocidas como GOT (Global Offset Table) y PLT (Procedure Linkage Table) que vamos a necesitar conocer caso de querer llevar a cabo una residencia per-process tal y como ya propusimos en Windows.

La GOT, mantiene direcciones absolutas (el código normal, que no ha de depender de la posición absoluta, no debería tenerlas, para poder ser compartido por distintos procesos). El programa obtendrá estos valores mediante un direccionamiento relativo, con lo cual se podrán redirigir llamadas a direccionamientos relativos a direcciones absolutas. Ojo, que estas direcciones absolutas no serán contenidas por el propio fichero (pues al compilarse no pueden saberse), sino que serán generadas por el linkador dinámico que es quien sabe de estas cosas

Ahora, la PLT (que va a utilizar a la GOT), va a tener como función la conversión de direcciones independientes de la posición del programa a direcciones absolutas; la transferencia de control entre distintos programas que funcionan de modo independiente a su posición es un problema que el linkador dinámico no va a poder resolver por sí mismo, con lo que va a necesitar de esta tabla. Si, suena algo abstracto, transferencia de control entre objetos distintos del proceso y tal, pero, ¿qué es sino una llamada a funciones de libc, por ejemplo? Pues llamar desde un bloque de código independiente de su posición (el programa ejecutable) a la memoria compartida en la que se halla libc (que también funciona independientemente de la posición). Y la traducción para que efectivamente pueda llamársele, va a descansar sobre la PLT y la GOT.

Cuando se crea la imagen en memoria del fichero, las posiciones de la GOT se rellenan con valores especiales, y se referenciará a esta GOT con una dirección absoluta o relativa (respecto a EBX) según el tipo de programa. Por ejemplo, pongamos que tenemos un fichero que sólo llama a una función; entonces, la segunda y tercera posiciones de la GOT se rellenan con unos valores especiales... y entonces, en este fichero que sólo llama a una función, tendríamos una PLT con este aspecto:

PLT0:  push [EBX+4]  jmp [EBX+8]  nop  nop PLT1:  jmp dword ptr [EBX+Funcion]  push $valor  jmp PLTO PLT2:  [...] 

La primera vez que llamemos a Función, lo que sucederá es que se transmitirá el control a "PLT1". Por tanto, saltará a la posición desplazada según el valor de "Función" de la GOT, que en esta ocasión (por ser la primera vez que se llama) apunta a la instrucción "push $valor" de la PLT, es decir, la siguiente instrucción al jmp indirecto que acabamos de hacer. El valor $valor que empuja a la pila es una referencia a la tabla de realocaciones, que será del tipo R_386_JMP_SLOT (su offset indicará el número de entrada de la GOT a la que hace referencia).

Tras empujar este offset saltamos a "PLT0", un trozo "común" de la PLT. Ahora, se empujará a la pila el valor de la posición [GOT+4h] (este valor es un identificativo del propio programa, para que el linkador dinámico pueda distinguir qué se ha de obtener) y se saltará a [GOT+8h], que es la dirección para llamar al propio linkador. ¿Qué va a hacer este linkador? Pues bien, cogerá la dirección absoluta a la que corresponde la función, y la colocará en su lugar correspondiente; es decir, que en esta ocasión en lugar de jmp dword ptr [EBX+Funcion] se situará un salto absoluto a la dirección de la función correspondiente.

Resumiendo los pasos de nuevo, puesto que puede resultar algo lioso de comprender:

1ª ejecución: Salta a PLT1, ejecuta un salto a la siguiente instrucción, la cual empuja un valor para ser referenciado en la tabla de realocaciones y salta a "PLT0". Una vez allí, empuja el segundo valor de la GOT en la pila y salta al tercer valor, que es la dirección del linkador dinámico. Este, sustituye el valor del salto que hay nada más caer en PLT1 por un salto absoluto sobre la función deseada. Luego, salta a esa dirección para llevar a cabo la función

2ª ejecución (y subsiguientes): Ya con el valor situado sobre el salto, la primera instrucción que antes era jmp dword ptr [EBX+Funcion] se sustituye por el salto absoluto a la función deseada.

Ahora bien, ¿cómo utilizar esto para nuestro provecho?. Este sistema es el utilizado por Linux para situar las llamadas a funciones pertenecientes al propio proceso, como pueda ser la siempre interesante función Exec(), por ejemplo, o las de apertura de ficheros y demás... ¿ya se os están poniendo los cuernos de diablillos?

Pues sí, por ahí tenemos el camino hacia la residencia "per-process", es decir, limitada al proceso que estamos ejecutando. Aquí llega, por supuesto, la inventiva individual; ¿el método más sencillo de implementar? Bien, si el virus se ejecuta antes que ninguna otra cosa, llamar a la función, dejar que el linkador dinámico rellene lo que tenga que rellenar y sobreescribir con la dirección de nuestro virus...

Pero bien, seguimos teniendo un problema; ¿cómo hacemos la relación de esto con los nombres de las funciones? Una opción es meter mano a la Dynamic Section (lo cual no es algo muy recomendable para los dolores de cabeza). Pero por suerte, aquel $valor que se empujaba a la pila, recordamos que era una referencia a la tabla de realocaciones,... pues bien, esta entrada contiene un indice respecto a la symbol table, lo cual nos permite saber qué función referencia la entrada.

El aspecto de una entrada de la symbol table es el siguiente:

typedef struct {  Elf32_Word st_name;  Elf32_Addr st_value;  Elf32_Word	st_size;  unsigned char st_info;  unsigned char st_other;  Elf32_Half st_shndx; } Elf32_Sym; 

¿Alguien se pregunta para qué puede servir un st_name? :-). Si a esa estructura nos da acceso una entrada en la tabla de realocaciones, tal como esta:

typedef struct {  Elf32_Addr r_offset;  Elf32_Word r_info; } Elf32_Rel;  

Entonces a la cosa no le queda mucho misterio... dos punteros, nada más, para recorrer el camino que recorre el dynamic linker a la hora de identificar la función respecto a su entrada en la tabla de realocaciones.

Y... por último, hemos de tener en cuenta a la variable de entorno LD_BIND_NOW. Si está desactivada todo será como acabo de contar. Si no es así, las referencias en la PLT estarán ya precalculadas cuando se empiece a ejecutar el programa.

Black Hole  
   
Facebook botón-like  
 
 
Hoy hay 26 visitantes¡Aqui en esta página!
Este sitio web fue creado de forma gratuita con PaginaWebGratis.es. ¿Quieres también tu sitio web propio?
Registrarse gratis