Infectores ELF: virus Windows frente a virus Linux
El virus binario se podría considerar como el estándar, el virus clásico: aquel que se propaga adheriendo su código a otros programas binarios.
Este tipo de virus era enormemente popular en las plataformas DOS y bastante usual en plataformas Windows, donde compiten en popularidad con los denominados virus "de macro" y con todo tipo de gusanos y troyanos que han surgido para esta plataforma en consecuencia de la explosión de Internet.
Simplificaciones y generalidades
No introduciremos en este grupo los infectores :VxD: o los módulos malignos, aunque en rigor se traten también de ejemplos de virus binarios (Recomendamos leer el artículo de Phrack "Bypassing Integrity Checking Systems", de halflife, acerca de la implementación de uno de estos módulos en Freebsd que engaña a Tripwire =:-O).
Otra sobresimplificación que aplicaremos es considerar en nuestro ejemplo una única forma de infección: la inserción de código en las zonas de relleno del segmento o segmentos ejecutables junto a la manipulación del punto de entrada.
Igualmente, consideraremos sólo un mecanismo de propagación: el código del virus se insertará en las víctimas durante los primeros momentos de ejecución del programa portador.
Estos supuestos son interesantes ya que darían virus válidos tanto en Linux como en Windows. Estas técnicas son aplicables a ambas plataformas por igual (ver código del Bizatch para Win32, p.ej).
Herramientas sugeridas
Es aconsejable disponer de un editor (o visor) hexadecimal para seguir las posteriores indicaciones, así como de un depurador y de un compilador de C si queremos "ver" los ejemplos en C.
El depurador de referencia sera el GDB, disponible en todas las distribuciones de Linux. El compilador que usaremos sera el GCC en su versión 2.95.1. Si no disponemos de un visor hexadecimal en nuestra distribución, podremos construirnos uno recurriendo al programa "hexdump" del paquete "util-linux".
En el siguiente script mostramos como hacerlo:
### #!/bin/sh # hexa, un sencillo visor hexadecimal # Uso: hexa [-c] [archivo] # -c: la salida del visor no será entubada hacia less # archivo: el archivo que analizaremos, por omisión stdin. if [ "$#" = "2" ] && [ "$1" = "-c" ] ; then PIPE="cat" ; shift else PIPE="less" fi (echo " 0001 0203 0405 0607 0809 0A0B 0C0D 0E0F" echo " ----------------------------------------------" hexdump $1 tr [a-f] [A-F] \ sed -e 's/ \([0-9A-F][0-9A-F]\)\([0-9A-F][0-9A-F]\)/ \2\1 /g') \ $PIPE ###
Ecosistema Linux
En esta sección se describen algunos puntos relevantes para la implementación de infectores binarios en Linux, buscando paralelismos con la plataforma Windows.
El formato ELF
ELF (Executable and Linking Format) es el formato binario estándar de Linux, de Solaris y de los SVr4 en general.
El nombre ELF nos sugiere una curiosa propiedad del formato: es "Ejecutable" y "Enlazable" (Linking). El mismo código objeto puede servir para ambas cosas: es decir, no es diferente el formato del código objeto generado para enlazar que el generado para ser ejecutado.
Un archivo en formato ELF se divide según este esquema:
- Cabecera
- Program Header Table
- *Segmentos*
- Section Header Table
Bastante limpio, ¿no? (Bueno, en realidad está simplificado, pero no creemos que incluir las secciones como .dynamic, .got, etc no añade detalle). Las estructuras que representan a cada una de ellas las podéis encontrar en "/usr/include/elf.h".
Si lo comparamos con el formato de ejecutable de Windows PE (Portable and Executable) vemos que es similar, aunque más complejo. PE incluye un primera parte de compatibilidad con DOS, que se encarga de mostrar el mensaje habitual cuando intentamos arrancar una aplicación Windows desde DOS, una cabecera opcional NT... Más la parte propiamente suya: cabecera, secciones, etc. En algunas de esas secciones se almacenan los recursos. Los iconos, por ejemplo: cuando desde el diálogo de selección de iconos picamos sobre un ejecutable, el icono se lee precisamente de una de estas zonas.
Una pequeña nota al margen: el formato binario adoptado para .NET seguirá siendo el PE, ampliado de forma que pueda manejar código escrito en C#. Se incluirá una nueva parte de compatibilidad con Windows anteriores (Que igual que en el caso de la de DOS se limitará a soltar un mensajito) más otra que cargará el runtime del lenguaje y posiblemente alguna cosilla más. Así que si ahora te parece complejo, espera uno o dos años y verás maravillas :-).
Esta complejidad añadida del PE otorga más flexibilidad al creador de virus a la hora de elegir un punto donde insertar su código.
Volvemos con ELF:
El campo cabecera (Elf32_Ehdr) es autoexplicativo. De los datos que contiene podemos enumerar la arquitectura para la que fue compilado el programa, el tipo de código (core, ejecutable, compartido, reubicable), el lugar físico del archivo donde comienza la "Program Header Table" y la "Section Header Table", el numero de entradas en cada una de ellas y el punto de entrada del programa.
Este último dato indica la dirección de memoria donde se cederá el control una vez cargado el programa en memoria. Adelantandonos un poco a los acontecimientos, nuestro virus guardará esta dirección para una vez ejecutado ceder el control al portador y la cambiará al infectar para que el primer código que se ejecute sea el suyo. Es el homólogo del RVA de entrada en la cabecera PE.
Podemos ahora echar un vistazo algunos de esos campos con un visor hexadecimal como hexa. Por ejemplo veamos "/usr/bin/whoami":
$ hexa /usr/bin/whoami
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
0000000 7F45 4C46 0101 0100 0000 0000 0000 0000
0000010 0200 0300 0100 0000 6086 0408 3400 0000
0000020 CC0D 0000 0000 0000 3400 2000 0500 2800
...
Los cuatro primeros bytes son el "número mágico" de ELF, (Un 7F seguido de los códigos ASCII de "ELF").
El byte 0x0010, 0x2, indica que se trata de un archivo ejecutable.
Y en 0x0018-0x001B tenemos el punto de entrada del programa (e_entry), que teniendo en cuenta que estamos en LSB, se trata de la dirección (virtual, por supuesto) 0x8048660.
Igualmente, el resto de los valores de la cabecera pueden ser obtenidos comparando la salida de hexa con los miembros de la estructura Elf32_Ehdr.
La "Program Header Table" tiene importancia si consideramos el código como "ejecutable" y no como "enlazable". Es una colección de estructuras Elf32_Phdr, cada uno describiendo un segmento: su tipo (PT_*, ver "/usr/include/elf.h"), su tamaño en disco, tamaño en memoria, su dirección de comienzo, sus permisos (lectura, escritura, ejecución), etc: información necesaria para que el sistema pueda crear la imagen del proceso al cargarlo.
En el ejemplo anterior con hexa, podíamos ver que esta tabla comenzaba en la posición del 0x0034 del archivo (bytes del 0x001C al 0x001F, e_phoff) y que tiene 0x5 entradas (0x002C-0x002D, e_phnum) de tamaño 0x20 bytes (0x002C-0x002D, e_phentsize). Vamos a ver la primera entrada:
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
...
0000030 1600 1500 0600 0000 3400 0000 3480 0408
...
Podemos ver que el segmento referenciado es de tipo 0x6 (0x0034-0x0037, p_type).
Mucho más interesante es la tercera entrada, correspondiente al tercer segmento, que comienza en 0x0074:
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
...
0000070 0100 0000 0100 0000 0000 0000 0080 0408
0000080 0080 0408 770B 0000 770B 0000 0500 0000
...
Cuyo tipo es 0x1 (PT_LOAD, el segmento "cargable") y cuyo desplazamiento es 0x0 (0x0078-0x007B). Los permisos de este segmento (los cuatro bytes del 0x008C al 0x008F, en este caso 0x5) nos indican permiso de ejecución y lectura:
PF_X PF_R == (1 << 0) (1 << 2) == 0x5
Como os estaréis imaginando, se trata del primer segmento de código. Será en él donde buscaremos el hueco para insertar nuestro código vírico.
El hueco sabemos que existe porque el tamaño de los segmentos es múltiplo del tamaño de página (PAGE_SIZE: 4kb, 0x1kb en x86) y es muy raro que elcódigo "real" se ajuste a ese tamaño perfectamente: lo normal es que quede una buena parte de espacio vacío, simplemente como alienamiento.
Otro tipo de segmento es el PT_INTERP (o tipo 0x3), que contiene el camino (terminado en 0) del "intérprete" del código. "Intérprete" en este contexto se refiere al cargador (a "/lib/ld-linux.so"). Este segmento (junto con otro PT_DYNAMIC) estará presente si, como es el caso, se trata de un ejecutable enlazado con bibliotecas dinámicamente.
La "Section Header Table" contiene información relevante al enlazado y es importante cuando el código se considera "enlazable" en vez de "ejecutable". Igual que la "Program Header Table", se trata de una tabla de estructuras (Elf32_Shdr en este caso) que describen una serie de segmentos (que incluye a los anteriores).
Informan de datos como posición del segmento en disco, tamaño en memoria, el nombre del segmento (En realidad un índice a la cadena correspondiente a la "StringTable" que no describiremos), alineamiento de las direcciones obligatorio o no (Hay micros que no funcionan si las direcciones que tienen que decodificar no están alineadas), la dirección virtual de comienzo del segmento, etc.
Sigamos inspeccionando whoami con hexa.
En la cabecera ELF se guarda el offset de la "Section Header Table", el numero de entradas y el tamaño de éstas, igual que vimos en el caso de la "Program Header Table". En este caso los números son:
La tabla comienza en la posición del 0x0DCC del archivo (bytes 0x0020-0x0023, campo e_shoff), tiene 0x16 entradas (0x0030-0x0031, e_shnum) y cada una ocupa 0x28 bytes (0x002E-0x002F, e_shentsize).
Destaquemos ahora algunas entradas:
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
...
0000FD0 0000 0000 7300 0000 0100 0000 0300 0000
0000FE0 789B 0408 780B 0000 0400 0000 0000 0000
...
El segmento referenciado tiene un offset 0x0B78 (bytes 0x0FE4-0x0FE7) y dirección virtual 0x8049b78 (0x0FE0-0x0FE3), su tipo es SHT_PROGBITS, lo que le etiqueta como "datos del programa", con permisos de escritura y con "memoria ocupada durante la ejecución" (0x0FDC-0x0FDF, SHF_WRITE SHF_ALLOC). En concreto, se trata del segmento .data: el segmento de datos.
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
...
0000F50 0000 0000 0400 0000 0400 0000 5F00 0000
0000F60 0100 0000 0600 0000 6086 0408 6006 0000
...
Offset 0x0660 (0x0F6D-0x0F6F), tipo SHT_PROGBITS, ejecutable, con "memoria ocupada durante la ejecución" (SHF_ALLOC SHF_EXECINSTR, 0x6) y con dirección virtual 0x8048660 (0x0F68-0x0F6B).
Retrocedamos un poco y veamos cual era el punto de entrada del programa (en la cabecera, el valor e_entry). Exactamente el mismo que la dirección virtual del segmento que referencia esta entrada: 0x8048660. Esto quiere decir que en el offset del archivo 0x0660 comienza el código del programa. Este segmento es en concreto ".text", el segmento de código. En algún punto del archivo, a partir del offset 0x0660 será donde insertaremos el código vírico.
El entorno de programación: las llamadas al sistema
En Linux, igual que en DOS, los servicios del kernel se aglutinan en torno a una interrupción. La forma de operar en Linux consiste en cargar los parámetros en registros e invocar después la interrupción 0x80.
En Windows (y en OS/2) la situación es diferente. Al hacer una llamada al sistema lo que haremos será apilar los parámetros y después hacer una llamada a subrutina (esto no es cierto, pero sirve como ejemplo. Ver más adelante). Esta forma de trabajo facilita hacer llamadas al kernel desde un lenguaje de alto nivel ya que los compiladores de C, Pascal, etc... Traducen sus llamadas a funciones al ensamblador siguiendo el mismo esquema. La programación a nivel de ensamblador se hace más tediosa, por todas esas instrucciones de manejo de la pila que tiene que escribir el programador humano. Para solucionar este problema, el ensamblador de Microsoft MASM incluye la macro "invoke", un caramelo sintáctico que permite traducir, por ejemplo, esta sentencia en C:
LoadMenu(hInst, MIMENU_ID)
Por esta otra en lenguaje esamblador:
invoke LoadMenu,hInst,MIMENU_ID
Sea como sea, está claro que en Windows para llamar a una subrutina del kernel debemos previamente conocer la dirección en donde reside dicha función. Más claro: no podemos hacer un "call" o un salto sin saber previamente en que dirección de la memoria donde se encuentra la primera instrucción de la subrutina que queremos llamar.
Desde el punto de vista del programador "usual", esto no es un problema. Podríamos figurarnos que, igual que el las llamadas a bibliotecas, el compilador calcula las direcciones de las rutinas y las inserta durante la traducción a lenguaje ensamblado, sustituyendo las etiquetas.
En realidad el formato PE es un poco más inteligente y las rutinas del kernel se tratan de forma distinta: las llamadas al sistema se traducen como saltos a posiciones de una tabla propia del proceso (la "ImportTable"), que cuando se carga el programa en memoria, el sistema se ocupa de rellenar con saltos a la dirección de cada rutina del kernel utilizada.
Es decir, una llamada a una función del kernel no se codifica como un "call dirección" sino como un salto corto a un punto de la "ImportTable". De esta forma se hace independiente nuestro código de las direcciones de las rutinas del kernel: lo que hacemos es apilar los parámetros y hacer un salto.
Sea como sea, todo esto para un programador normal es totalmente transparente, no tiene que preocuparse de ello.
Sin embargo, los virus se diseñan para ser insertados posteriormente a la compilación que es cuando queda fijada la "ImportTable" (Hasta donde yo sé, es imposible modificarla a posteriori) así que o bien usan las mismas rutinas del sistema que el programa anfitrión (Algo que no es práctico) o deben saber de antemano cual es la dirección de las llamadas al sistema que utilicen.
Para dar con esas direcciones, la solución más obvia pasa por coger el depurador y echar un vistazo a un programa compilado que haga uso de lo que necesitemos: una vez conocidas podemos introducirlas directamente en el código del virus.
El problema es que así nos estamos saltando la "ImportTable", nuestro código depende las direcciones reales de las rutinas y esas direcciones sólo son validas para un determinado kernel, pudiendo cambiar entre versiones: por ejemplo, no tienen los mismos puntos de entrada los kernels de Windows95 que los del Windows98 y de hecho, si no fuera por la "ImportTable", un programa Windows 95 no podría funcionar en un Windows 98 sin recompilar (y viceversa). De ahí le viene a PE lo de "portable".
Entonces, ¿no hay virus multiversión sobre Windows?. Sí, sí los hay, pero recurren a otras técnicas más complicadas. En cualquier ezine sobre el tema obtendréis cumplida información: lo más usual es mirar la "ImportTable" y "GetModuleHandle", pero hay muchas variantes.
En Linux, este problema no existe puesto que las llamadas al sistema se realizan a través de una interrupción (siempre la misma) y el paso de los parámetros por los registros. Al menos en este sentido programar un virus en Linux es algo más simple que hacerlo en Windows, al no tener que pelearse con direcciones de entrada
¿Qué ocurre con la funciones de biblioteca (dinámica)?. En este caso, en Linux la causística es idéntica a la de Windows: no podemos llamar a la función primero si no sabemos si está cargada su biblioteca en memoria y segundo si no conocemos la dirección de la función. Al final lo que tenemos (salvo hábil hack, que los hay) es que el virus es libre de utilizar las llamadas al sistema pero no funciones de biblioteca.
ELF dispone de dos tablas similares de alguna forma a la "Import Table" pero en funciones de biblioteca (una tabla que "traduce" direcciones relativas a direcciones absolutas). Se trata de la .plt ("Procedure Linkage Table") y la .got ("Global offset table"). Una de las tareas del cargador "/lib/ld-linux.so" (del que hablamos hace tiempo en referencia a los sonames) es ajustar esta tabla en el momento de cargar el programa en memoria.
Es posible jugar con las dos tablas de forma que puedan redireccionarse llamadas a una función de una biblioteca dinámica, por ejemplo getpass() de lib, a otra "malvada". Esto es trivial hacerlo a través del mecanismo LD_PRELOAD, pero eso es bastante complejo de realizar sobre el binario ya compilado y enlaazado, y no será tratado en este documento. Para más detalle, Silvio Cesare documentó esta técnica en un articulo de Phrack sobre infecciones .plt.
El código de un ¿virus?
De acuerdo ansiosos: ahí va :-)
"\x56\x53\x52\xeb\x1e\x59\xbb\x01\x00\x00\x00" "\xba\x13\x00\x00\x00\xbe\x04\x00\x00\x00\x89" "\xf0\xcd\x80\x5a\x5b\x5e\xb8\x00\x00\x00\x00" "\xff\xe0\xe8\xdd\xff\xff\xff\x48\x6f\x6c\x61" "\x2c\x20\x73\x6f\x79\x20\x75\x6e\x20\x76\x69" "\x72\x75\x73\x0a"
X-)
Este es el "malvado" código que insertaremos en los archivos binarios. Si al lector no entiende que quiere decir esa ristra de bytes baste decir que se trata de un programita sencillo ya en código máquina (esto es, previamente ensamblado).
Programas así representados los hay a miles: son uno de los pilares en los que se basan los ataques por stack overflow (el otro es que el segmento de pila tiene permisos de ejecución). Si deseáis profundizar sobre el tema os recomiendo el excelente artículo del ezine Phrack: "Smash stack for fun and profit", firmado por Aleph One (buscad entre los números antiguos de Phrack en ).
Traducido línea a línea, en sintaxis AT&T:
"\x56" /* pushl %esi */ "\x53" /* pushl %ebx */ "\x52" /* pushl %edx */ "\xeb\x1e" /* jmp 0x19 */ "\x59" /* popl %ecx */ "\xbb\x01\x00\x00\x00" /* movl $1, %ebx */ "\xba\x13\x00\x00\x00" /* movl $13, %edx */ "\xbe\x04\x00\x00\x00" /* movl $4, %esi */ "\x89\xf0" /* movl %esi, %eax */ "\xcd\x80" /* int $0x80 */ "\x5a" /* popl %edx */ "\x5b" /* popl %ebx */ "\x5e" /* popl %esi */ "\xb8\x00\x00\x00\x00" /* movl $0x0, %eax */ "\xff\xe0" /* jmp *%eax */ "\xe8\xdd\xff\xff\xff" /* call -0x1e */ "\x48\x6f\x6c\x61\x2c\x20\x73\x6f\x79\x20\x75\x6e\x20\x76" "\x69\x72\x75\x73\x0a"
Los últimos bytes son el mensaje (en ASCII) que desplegaremos.
Algunos detalles sobre el código:
Básicamente se trata de una llamada a la rutina del kernel "write", que recibe el descriptor de archivo donde escribir en ebx (En este caso en stdout, 0x1), la dirección de la cadena en ecx y su longitud (0x13, 19 caracteres) en edx. El registro esi se utiliza para indicar la llamada al sistema que vamos a utilizar (en este caso __NR_write o 0x4, consultar /usr/include/asm/unistd.h para encontrar una referencia completa del los código de las llamadas al sistema).
Para obtener la dirección de la cadena se recurre a un truco perfectamente documentado en el anterior "Smash stack for fun and profit".
Resumiendo: en principio desconocemos dónde residirá este código al final de la infección así que no podemos cargar en ecx la dirección de la cadena directamente con un "movl" porque no podemos saberla de antemano. Para solucionar esto utilizamos un salto relativo ("jmp 0x19") a una llamada también relativa justo antes de la cadena ("call -0x1e"). Esto nos asegura que la dirección de la cadena se encontrará en la pila justo después del "call", y el "call" nos devuelve al punto "popl ecx" donde cargamos por fin dicha dirección en el registro.
Otro detalle es cómo se maneja la salida del código "maligno":
"\xb8\x00\x00\x00\x00" /* movl $0x0, %eax */ "\xff\xe0" /* jmp *%eax */
Por supuesto un salto a 0x0 no es válido y de realizarse el micro provocará una interrupción y Linux una señal SIGSEGV en consecuencia.
La idea es que, antes de infectar, leamos la dirección de comienzo del código original del programa (En el miembro de la cabecera ELF, e_entry) y reescribamos ese 0x0 con dicha dirección. Como resultado, una vez ejecutado el write cederemos el control al programa original.
Y ¿dónde está el código para la infección?. Bien, ahí no está, desde luego: si miráis el adjunto veréis que hemos escrito un programa aparte, en C, que se encargará de infectar el archivo binario con el código que le indiquemos.
Evidentemente, un virus real no funciona así: él tiene codificado sus rutinas propias de infección y cuando estima oportuno infecta a sus víctimas sacando una copia de si mismo.
Desde luego, cualquiera que tenga una formación media-alta en lenguaje ensamblador está capacitado para modificar el ejemplo de manera que actúe de esa forma y si alguien se decide a hacerlo, me gustaría que nos mandase una copia de su trabajo.
Después de la infección
Podemos visualizar con hexa los cambios introducidos por el "virus".
$ infector whoami $ hexa whoami.inf
Infectado. El offset a partir del que cual se introdujo el código vírico al final fue 0x0B77 (podéis verlo con GDB).
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
--- Aquí comenzamos.
v
...
0000B70 4944 2025 750A 0056 5352 EB1E 59BB 0100
0000B80 0000 BA13 0000 00BE 0400 0000 89F0 CD80
...
Esos "números" no aparecen en el whoami no infectado (evidentemente):
$ hexa whoami
0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
--------------------------------------------
...
0000B70 4944 2025 750A 0000 0000 0000 FFFF FFFF
0000B80 0000 0000 FFFF FFFF 0000 0000 D09B 0408
...
Y el resulta al ejecutar fue el esperado:
$ ./whoami.inf Hola, soy un virus jorge
Resumen del procedimiento de infección
Según las propias palabras de Silvio Cesare:
- Aumentamos el valor de p_shoff en PAGE_SIZE, así nos aseguramos que podremos insertar el virus en cualquiera de los segmentos sin pisotear la "Section Header Table".
- Modificamos el 0x0 "mov $0x0,eax" del virus por el punto de entrada leído de la cabecera.
- Buscamos la estructura que representa el segmento de código.
- Modificamos el punto de entrada de la cabecera por el resultado de la suma de su p_vaddr y p_filesz.
- Aumentamos p_filesz en tantos bytes como ocupe el virus.
- Idem para p_memsz. Con estos dos pasos nos aseguramos que el sistema reserve el suficiente espacio para el segmento modificado.
- Aumentamos todos los p_offset de las estructuras Elf32_Phdr restantes, a partir del segmento en el que hemos introducido el virus, en PAGE_SIZE. De otra forma, los segmentos a continuación de .text quedarían descuadrados.
- Aumentamos el valor de sh_len en tantos bytes como tenga el virus, en la última entrada Elf32_Shdr de .text.
- Incrementamos todos los sh_offset de las estructuras Elf32_Shdr a continuación de la que representa el segmento donde introdujimos el virus.
- Por último: escribimos la cabecera (modificada), la "Program Header Table" (modificada), el código del original hasta el offset del virus, el virus más los suficientes ceros de alineamiento hasta rellenar la página, el código hasta la "Section Header Table", la "Section Header Table" (modificada) y el resto de lo que aparece en el original en la versión infectada de la víctima.
Limitaciones
Primero: esta forma de infección es muy sencilla de entender pero también es muy sencilla de detectar. ¡Es muy sospechoso un punto de entrada que no se corresponde con ningún segmento!.
Segundo: el tamaño de los archivos se incrementa en un poco más de 4kb (el tamaño de página). Esto es muy sospechoso en archivos tan pequeños como whoami.
Tercero: hay algunos campos más dentro de ELF que no hemos modificado y que introducen un funcionamiento errático.
Por ejemplo, no hemos tocado para nada la "String Table" y esto hace que cualquier programa que intente mostrar el nombre de los segmentos (como el depurador) no sea capaz de hacerlo.
Veamos como opera gdb en esto:
$ gdb whoami (gdb) info files Symbols from "/home/jorge/Proyectos/linvirus/infector3.ex/whoami". Local exec file: /home/jorge/Proyectos/linvirus/infector3.ex/whoami Entry point: 0x8048660 0x080480d4 - 0x080480e7 is .interp 0x080480e8 - 0x080481ac is .hash 0x080481ac - 0x0804838c is .dynsym 0x0804838c - 0x080484b5 is .dynstr 0x080484b8 - 0x080484c0 is .rel.got 0x080484c0 - 0x080484e0 is .rel.bss 0x080484e0 - 0x08048548 is .rel.plt 0x08048550 - 0x0804857c is .init 0x0804857c - 0x0804865c is .plt 0x08048660 - 0x080489a0 is .text 0x080489a0 - 0x080489bc is .fini 0x080489bc - 0x08048b77 is .rodata 0x08049b78 - 0x08049b7c is .data 0x08049b7c - 0x08049b84 is .ctors 0x08049b84 - 0x08049b8c is .dtors 0x08049b8c - 0x08049bd0 is .got 0x08049bd0 - 0x08049c58 is .dynamic 0x08049c58 - 0x08049ce8 is .bss (gdb) q $ gdb whoami.inf... varios errores de la biblioteca BFD "invalid string offset" ...
(gdb) info files Symbols from "/home/jorge/Proyectos/linvirus/infector3.ex/whoami.inf". Local exec file: /home/jorge/Proyectos/linvirus/infector3.ex/whoami.inf, Entry point: 0x8048b77 0x080480d4 - 0x080480e7 is 0x080480e8 - 0x080481ac is 0x080481ac - 0x0804838c is 0x0804838c - 0x080484b5 is 0x080484b8 - 0x080484c0 is 0x080484c0 - 0x080484e0 is 0x080484e0 - 0x08048548 is 0x08048550 - 0x0804857c is 0x0804857c - 0x0804865c is 0x08048660 - 0x080489a0 is 0x080489a0 - 0x080489bc is 0x080489bc - 0x08048bb2 is 0x08049b78 - 0x08049b7c is 0x08049b7c - 0x08049b84 is 0x08049b84 - 0x08049b8c is 0x08049b8c - 0x08049bd0 is 0x08049bd0 - 0x08049c58 is (gdb) q
Como vemos, nuestro código no es precisamente elegante :-(.
Y una cuarta limitación (derivada en parte de la anterior) es que el archivo infectado no soporta un "strip".
$ strip whoami.inf... varios errores "invalid string offset" ...
$ ./whoami.inf Segmentation fault (core dumped)
Igual que comenté al principio: si alguien tiene interés en técnicas que permitan evitar estos problemas, puede consultar la documentación de Silvio Cesare en Phrack.
Despedida y cierre
Y eso era (casi) todo lo que tenía que contaros acerca de los virus binarios en Linux. Lo que falta sobre virus de macro lo incluiré en una próxima entrega.
Tenéis el código que se referencia en el artículo en este enlace (infector.tar.gz). Para probar el ejemplo, basta con que hagáis:
$ tar -tzf infector.tar.gz && cd infector $ make $ cp /usr/bin/whoami . $ ./infector whoami $ ./whoami.infiu
El código del ejemplo usa dlopen() para implementar "virus-plugins" (ver main.c). Si queréis experimentar con vuestros propios infectores, basta con que reemplaceis ese "ejemplo.so" por el vuestro (o podéis reescribir un poco main.c para que acepte el nombre del infector como parámetro).