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
Técnicas avanzadas

Atras


 

9.0.0.- Introducción

En este capítulo se explicarán dos técnicas aplicables a la programación de virus informáticos que pueden ser utilizadas en diversos contextos y sistemas operativos. Comenzaremos hablando sobre encriptación básica, para luego hablar de aquello que quizá constituye una de las partes más personales que un escritor de programas autorreplicantes puede crear; el polimorfismo.

 

 

9.1.- Encriptación

 

9.1.1.- Encriptación simple

Resulta un poco ridículo que, tras tener nuestro virus acabado, cualquiera pueda coger con un editor de textos, abrir el programa infectado y encontrar una cadena de texto diciendo "Virus Loquesea escrito por tal" y demás texto que hayamos incluído dentro. Además, sin un sistema siquiera mínimo de encriptación hasta el antivirus más estúpido puede darnos una alarma con su heurística. Por tanto, la encriptación se convierte en algo casi necesario para nuestros pequeños autorreplicantes.

El sistema más habitual de encriptación consiste en la utilización de una función XOR o de una combinación ADD/SUB.

La XOR, que suele ser la más habitual, se basa en el hecho de que cuando a un número le aplicamos dos veces la operación XOR con el mismo número, el resultado acaba siendo el mismo que al principio. Es decir, supongamos que A = 10101101 y B = 11011011. Ahora operamos:

A XOR B = 10101101 XOR 11011011 = 01110110

(A XOR B) XOR B = 01110110 XOR 11011011 = 10101101

Este es quizá el método de encriptación más sencillo que podemos encontrar en un virus informático; al hacer el XOR respecto de B se obtiene un nuevo valor que es ese byte encriptado, y de nuevo haciendo XOR con B se vuelve al original. Por lo tanto, para llevar adelante esta técnica sólo tendremos que almacenar un determinado valor (preferiblemente aleatorio) que sea este B, e iterar por todos los bytes del virus para encriptarlo. Un ejemplo en código sería este:

lea esi, [inicio_encriptado+ebp] mov ecx,tamanyo_encriptado call Generar_Num_Aleatorio ; supongamos que nos lo devuelve en AL mov byte ptr ds:[llave_encriptado+ebp],al  Loop_Encriptacion: xor byte ptr ds:[esi],al inc esi loop Loop_Encriptacion

Ahora bien, la parte del virus a ser encriptada evidentemente no puede ser su totalidad; tiene que haber un trozo que no sea encriptado, puesto que ha de haber alguna forma en que este código sea desencriptado para poder ejecutarse. Realmente, sólo la parte que desencripta no debe ser encriptada con lo que el resto sí puede serlo. Así, podríamos por ejemplo copiar el virus al fichero infectado mediante memory file mapping y encriptarlo *en el fichero* pero no en la zona que estamos ejecutando nosotros. Al inicio del virus podríamos dejar sin encriptar algo como esto:

call Delta Delta: pop ebp sub ebp, offset Delta lea esi, [inicio_encriptado+ebp] mov ecx,tamanyo_encriptado mov al, byte ptr ds:[llave_encriptado+ebp]  Loop_Desencriptacion: xor byte ptr ds:[esi],al inc esi loop Loop_Desencriptacion

Con estas lineas ya podríamos desencriptar en la siguiente ejecución la totalidad del virus; todo lo que no sean estas líneas de código quedarían ocultas a ojos curiosos (ojos no lo bastante curiosos como para coger un debugger y ejecutar estas lineas que lo desencriptan, claro). Por supuesto, tendréis el problema de que en la primera generación del virus (nada más compilarlo) este no está encriptado. Nada más sencillo para arreglar esto que inicializar "llave_encriptado" a cero.

Otros sistemas sencillos de encriptación que son variantes sobre este serían por ejemplo el uso de instrucciones de desplazamiento (ROR/ROL), de suma y resta (sumando al encriptar y restando al dividir la misma cantidad o viceversa), o el uso de un simple NOT (equivalente a XOR reg,0FFh).

Y por cierto, un detalle curioso es que a pesar de su sencillez, el algoritmo XOR de encriptación podría ser un algoritmo invencible para ocultar las comunicaciones si se cumpliesen dos condiciones; por un lado la seguridad del canal por el que se transmite la clave y por otro la posibilidad de generar números verdaderamente aleatorios. Si encriptamos cada byte de los datos con un número distinto, y todos los números que utilizamos para encriptar forman parte de una cadena totalmente aleatoria (en la práctica, la mayoría de sistemas que utilizan esto se basan en un algoritmo que si no es totalmente aleatorio es al menos impredecible, basándose en valores numéricos acerca de la radioactividad nuclear y su decaimiento). La explicación es sencilla; si la información tiene un aspecto absolutamente aleatorio gracias a la aleatoriedad de la llave que los codifica (la cadena de números), no existe ninguna forma de obtener un mensaje coherente a partir de ello excepto obteniendo la cadena de números que se utilizó para encriptar...

A veces, los mejores métodos son los más sencillos; pero un algoritmo que realice números pseudo-aleatorios, tan sólo dará lugar a un sistema de encriptación pseudo-seguro. Nunca olvidemos que si hacemos un XOR entre un byte encriptado y uno desencriptado, obtenemos el número que ha sido utilizado para encriptar (cualesquiera dos componentes de una encriptación XOR dan lugar al tercero).

 

 

9.1.2.- Generación de números aleatorios

Gran parte del éxito de un sistema de encriptación o un engine polimórfico (de los cuales hablamos más adelante) se basa en el hecho de que posea un buen sistema de generación de números aleatorios. Si siempre ponemos el mismo número a la hora de encriptar será mucho más fácil cazarnos, y en engines polimórficos la necesidad de una buena rutina de este estilo ya se hace absoluta.

Aunque es prácticamente imposible generar números realmente aleatorios - indiqué hace un par de párrafos el sistema aproximado que más se suele usar y el lector puede ser consciente de su complejidad -, el sistema más típico consiste en buscar un valor realmente aleatorio que pueda usarse como semilla; es decir, que las siguientes veces que necesitemos números aleatorios los sacaremos haciendo operaciones sobre este valor.

La obtención bajo un SO como Linux de esto es sencilla, puesto que por defecto la útil instrucción RDTSC no tiene privilegios de supervisor en el procesador. Esta instrucción, devuelve un contador de gran precisión que puede perféctamente servir como semilla aleatoria; ¿el problema? que existe la opción de hacer que sólo pueda ejecutarse en modo supervisor (¿no entiendo por qué?) y esto en Windows es así mientras que, por lo general, no sucede en Linux.

Bajo Windows quizá el mejor sistema sería una llamada a la función de sistema GetTickCount que hace básicamente lo mismo que RDTSC pero con una llamada a API en vez de con una instrucción (viva la optimización), cuya especificación muestro a continuación:

 

DWORD GetTickCount(VOID)

Y simplemente (jejeje sencillita de llamar eh xD) muestra el número de milisegundos desde que Windows se inició... un consejo no obstante, no es conveniente si tenemos que obtener varias veces seguidas números aleatorios llamar contínuamente a esta función, en cualquiera de sus dos formas. Quizá, el contador no haya tenido tiempo de cambiar o lo haya hecho muy ligeramente, con lo que no estaríamos generando un número precisamente muy aleatorio.

Por tanto, la mejor política es una primera llamada para inicializar el valor de semilla aleatoria y después operaciones sobre ese número que se hagan cada vez que se llama a vuestra función de obtener un número aleatorio, operaciones que tendréis que diseñar de modo que creen una aleatoriedad realmente decente... pero esto ya es algo que entra en el desarrollo que realice cada uno...

 

 

9.1.3.- Encriptación Avanzada

Utilizar sistemas de encriptación más avanzados (no parece muy difícil que sean más complejos que un Xor pero en fin xD) tiene ventajas y desventajas. Las ventajas son claras; estamos ocultándonos de un modo más robusto y los emuladores de los antivirus lo van a tener más difícil para desencriptar nuestro código (los detectores de virus medianamente decentes hoy en día intentan "emular" el código que encuentran para que él mismo se desencripte y poder detectar con facilidad el cuerpo desencriptado del virus). La desventaja es que vamos a tener una rutina de desencriptación mucho más larga, lo cual si vamos a querer aplicar posteriormente técnicas de polimorfismo es bastante más difícil (aunque nadie ha dicho que no se puedan poner varias capas de encriptación unas encimas de otras, la más sencilla con el polimorfismo más fuerte).

Explicaré de forma relativamente breve los sistemas de encriptación más importantes que pueden tener aplicación en un sistema como este. Podríamos pensar en complicarnos la vida con sistemas tipo RSA, pero lamentablemente y como es lógico, si el virus tiene la llave privada para desencriptarse, el antivirus también podrá acceder a ella (lo cual nos sucede de nuevo como digo, lamentablemente, en cualquier sistema que utilicemos dado que la clave de desencriptado ha de estar autocontenida en el virus)... se han hecho en ocasiones aproximaciones distintas a este hecho, como el RDAE (Random Decryption Algorithm Engine) de Darkman, que prueba aleatoriamente combinaciones hasta que se desencripta intentando hacer que al antivirus no le compense trabajar tanto sin haberlo detectado (encripta con un valor aleatorio que no guarda). Aunque lamentablemente no se trata de una técnica demasiado efectiva, es un esfuerzo interesante en este camino.

Pasemos entonces a hablar de cifrado por bloques y algoritmos tipo hash:

 

Cifrado por bloques: Los sistemas basados en cifrado por bloques pueden constituir un algoritmo de encriptación que dé quebraderos de cabeza a quien quiera eliminar el virus (estando por supuesto contenido parte del código del fichero infectado dentro de la zona encriptada). Creados en los 70 por IBM, su primer resultado se llamó "Lucifer", con una clave de 128 bits que coincidía con el tamaño del bloque (aunque ninguna de sus variantes era segura). Cada paso que se realiza sobre un bloque se denomina ronda (round), y se supone que cuanto mayor es el número de rondas realizadas sobre cada bloque, mayor es la seguridad... el ejemplo más clásico de este tipo de cifrado es el sistema DES, que tiene una clave de 56 bits y actúa en 16 rondas sobre bloques de 64 bits (por cierto, este algoritmo fue diseñado por IBM con... ayuda de la NSA, en fin, este último organismo es una constante en muchos algoritmos).

A cada bloque que se cifra se le aplica una determinada función cierto número de veces (las rondas ya mencionadas); como ejemplo de implementación de un sistema de cifrado por bloques (que aporta complejidad a la hora de desencriptar y desinfectar pero no es en absoluto infalible ya que el antivirus puede leer la llave que se utilizó para encriptar) se puede mirar mi QBCE en el virus "Win9x.Unreal".

Volviendo al tema, estos sistemas son vulnerables a ataques que busquen correlaciones entre el input/output al aplicarse la función (criptoanálisis diferencial), respecto a la llave y el output (criptoanálisis lineal) y en base a correlaciones en cambios en la clave y el output (criptoanálisis basado en la clave). En cualquier caso esto no nos importa mucho; el antivirus sólo tiene que aplicar el mismo algoritmo que nosotros, pues conoce la llave utilizada - sólo podremos hacerlo algo más lento.

Es interesante el detalle de que el criptoanálisis diferencial se inventó en 1990, y todos los sistemas anteriores a esa fecha son vulnerable... todos excepto el DES, desarrollado 15 años antes, porque ya entonces IBM y la NSA conocían esta forma de ataque (pero jamás la hicieron pública... en cualquier caso, DES es inseguro y existe hardware que lo desencripta casi instantáneamente).

Otro algoritmo que merece la pena mencionar dentro de esta clasificación es el IDEA, que fue utilizado en un virus del programador Spanska. Una descripción de ese autorreplicante debería poder encontrarse en http://www.avp.ch/avpve/file/i/idea.stm

 

Funciones Hash: Se trata de algoritmos que llevan a cabo operaciones con una serie de datos de entrada de diversos tamaños reduciéndolos a un sólo valor, de modo que la función es irreversible y su resultado virtualmente único. Encontramos varios algoritmos que cumplen esta labor, como MD2, MD4, MD5, SHA, HAVAL o SNEFRU. De ellos, suele aceptarse SHA como el más seguro (cómo no, desarrollado por la NSA). MD2 y MD4 han sido rotos, y MD5 se considera que tiene debilidades...

Este sistema, por ejemplo, se utiliza con las claves almacenadas en el fichero de passwords de nuestro Linux casero; las claves de los usuarios no se almacenan, sino que lo que se guarda es el resultado de aplicar este tipo de función, de modo que cuando se intenta acceder a la cuenta de un usuario a la clave que se nos pregunta se le aplica el algoritmo y se compara el resultado con el almacenado (es por esto que los ataques más típicos cuando se obtiene este fichero de passwords se basan en codificar diccionarios con el algoritmo que se utilice en el sistema para compararlos con las entradas en el fichero de passwords; si se encuentra una coincidencia, se ha descubierto la clave).

Esta aproximación ha sido utilizada en más de un virus para ocultar cadenas de texto a comparar con otras, de modo que se almacene tan sólo el resultado de la función utilizada y se aplique esta función a las cadenas de texto/codigo/etcétera con que se quiera compararlo. Por ejemplo, si queremos evitar que nuestro virus infecte determinados ejecutables pertenecientes a programas antivirus, en lugar de comparar el nombre del fichero con 'ANTIVIRUS.EXE' conteniendo esa cadena en nuestro código, almacenaríamos el valor resultante de aplicarle esta función (p.ej, supongamos que fuera "A1cHypr3F0"). Así, al ir a infectar aplicaríamos el algoritmo al nombre del fichero a infectar, y si el resultado fuera ese "A1cHypr3F0", no lo infectaríamos.

Muchos de estos algoritmos son públicos, y se pueden encontrar referencias con explicaciones sobre su funcionamiento y código para llevar a cabo su función (aunque probablemente estará en lenguaje C y tendréis que encargaros de reprogramarlo en ensamblador). Existe también una forma de hacer más complejo este algoritmo, lo que se denomina "HMAC" (Hash Message Authentication Code), sistema por el cual el hash se realiza respecto a una clave de forma que esta afecte tanto al inicio como al final del proceso de cifrado; no obstante no hace falta esforzarse mucho en ello si estamos programando virus, ya que esta clave al estar disponible para nosotros lo estará también para el antivirus.

 

 

 

 

9.2.- Polimorfismo

 

9.2.1.- Introducción al polimorfismo

Hace ya unos cuantos años, un programador búlgaro que se hacía llamar Dark Avenger anunció a las compañías antivirus que en breve sacaría un sistema por el cual un virus podría tener millones de aspectos distintos. Se rieron de él, pero no tardó mucho esa sonrisa en congelarse; hasta entonces los programas antivirus no eran más que buscadores de cadenas hexadecimales. Si su registro de cadenas hexadecimales coincidía con el de algún fichero, detectaba el virus en particular. Al fin y al cabo, aunque se encriptara el código del virus, había siempre una parte que permanecía constante... la rutina de desencriptado. El antivirus detectaba estas rutinas y las utilizaba para descifrarlo.

Sin embargo, a Dark Avenger se le había ocurrido algo que aún da muchos dolores de cabeza a las compañías antivirus: el polimorfismo. Su sistema consistía en hacer que la rutina de desencriptado también fuera complétamente variable, de modo que no pudiera ser detectada como una cadena hexadecimal.

Para ilustrar en qué consiste el polimorfismo, nada mejor que usar un ejemplo de rutina de desencriptado e ir variándola. Veremos cómo, en todas sus formas, esta rutina siempre está haciendo lo mismo:

mov ebp, valor_delta lea esi, [inicio_virus+ebp] mov al, byte ptr [valor] mov ecx, <tamaño> descifrar: xor byte ptr [esi], al inc esi loop descifrar

Cuando esto era así de forma inmutable, el antivirus no tenía más que buscar, en este caso, la cadena de bytes que resultaba del compilado de estas líneas. Sin embargo, veamos esto ahora:

push valor_delta pop edi add edi, inicio_virus mov cl, byte ptr [valor] mov ebx, <tamaño> descifrar: xor byte ptr [edi], cl inc edi dec ebx jnz descifrar

Si se observa detenidamente este código, es fácil ver que está haciendo exáctamente lo mismo que la rutina anterior; hemos empujado el valor precalculado del delta_offset, lo hemos sacado en edi al que añadimos "inicio_virus" (haciendo las veces de ESI en el ejemplo inicial). Además, en vez de AL usamos CL para desencriptar; tampoco utilizamos un loop sino un dec/jnz respecto a ebx. Estamos, pues, haciendo lo mismo con un código distinto.

Otro ejemplo:

mov edi, (inicio_virus+valor_delta+tamaño) mov bl, byte ptr [valor] mov edx, <tamaño> descifrar: xor byte ptr [edi], bl dec edi dec ebx jnz descifrar

Ahora, estamos desencriptando desde el final hacia el principio en lugar de como haciamos antes... y las posibilidades no acaban aquí, como es evidente; podríamos hacer que a veces se utilizara el xor y otras el add/sub. También podemos utilizar otros registros, idear distintas formas de cargar los parámetros en los registros correspondientes, u otras maneras en que nuestro código fuera desencriptado...

Sin embargo, esto no es todo lo que el polimorfismo tiene que ofrecer; esta variación en la rutina de desencriptado es sólo el primer paso.

 

 

9.2.2.- Generación de código basura

En realidad, tampoco se lo estamos poniendo tan difícil como podemos aún a los detectores de virus con lo que hemos visto en el apartado anterior. El siguiente paso en complejidad en el polimorfismo, es la generación de instrucciones basura.

¿Qué son instrucciones basura? Pues bien, se trata de código que no va a hacer nada útil, más que despistar a los detectores de virus. Por ejemplo, como ejercicio básico podríamos marcar aquellos registros que no utilizasemos en nuestra rutina de desencriptado y utilizarlos para operar sobre ellos... podemos hacer lo que queramos, puesto que al ser los primeros en ejecutarnos no tenemos restricciones respecto a estos registros.

Pongamos, por ejemplo, que utilizamos EAX, ECX y EDX. Esto nos deja disponibles el resto de registros para generar instrucciones como MOV EBX,<valor> o ADD ESI, ECX (¡con esta instrucción ESI es alterado pero ECX no!), y todo un enorme rango de operaciones que no van a suponer ningún problema ni para nosotros ni para la ejecución de nuestro virus.

Considerando esto, que constituiría el polimorfismo más clásico, ya podemos ver esta técnica como una de las más personales de las que puede desarrollar un escritor de programas autorreplicantes; no vamos a tener más remedio que fabricarnos tablas de opcodes (códigos de operación) para generar instrucciones nosotros mismos, unido a una estrategia de variación de las rutinas de desencriptado que intente resultar en un todo coherente y efectivo contra la detección de nuestro bichito.

Podemos basar este código en tablas, en subrutinas con saltos condicionales, o en cualquier método que nos parezca más cómodo o efectivo. Por ejemplo, podríamos tener una zona "central" del engine polimórfico que se dedicara a saltar aleatoriamente a distintas funciones para generar código basura. Una de ellas podría ser esta:

; suponemos que ; EDI = lugar donde generar  Genera_Reg_Imm:  mov al, 0B8h call Genera_Aleatorio ; valor devuelto en ECX and cl, 07h ; hace que el valor vaya de 0 a 7 add al, cl stosb call Genera_Aleatorio stosd ret

Ahora, ¿qué hace este código?. 0B8h es el código de operación de MOV EAX, valor_inmediato, y le estamos añadiendo un valor aleatorio entre cero y siete (nuestro generador aleatorio fabrica números de 32 bits). La razón la entendemos si miramos la siguiente tabla:

 

 
Código
 
Acción
0B8h
MOV EAX, valor
0B9h
MOV ECX, valor
0BAh
MOV EDX, valor
0BBh
MOV EBX, valor
0BCh
MOV ESP, valor
0BDh
MOV EBP, valor
0BEh
MOV ESI, valor
0BFh
MOV EDI, valor

 

Efectivamente, cuando el procesador ve un byte con uno de estos valores, lo interpreta como "mover a tal registro tal valor". Por tanto, leerá los cuatro siguientes bytes para averiguar cuál es ese valor. Internamente moverá ese valor al registro y continuará su ejecución.

Nosotros hemos averiguado que estos opcodes, los que van del rango 0B8h a 0BFh, sirven para mover un valor a un registro; por tanto en la rutina que habíamos fabricado y que se llamaba de forma aleatoria por un CALL, vamos a generar una órden tipo MOV <reg>,<valor> aleatoria.

Aun así, un código como el mostrado anteriormente no puede quedarse así. Hay registros que nosotros estamos utilizando y que no queremos que sean modificados. Además, tampoco nos conviene modificar el registro de pila, ESP, puesto que entonces si estaríamos interfiriendo con la ejecución del programa - y de nuestro propio virus. Tendremos por tanto que hacer una comprobación de si el valor generado es 0BCh (mov ESP, <valor>) y volver al principio de la rutina si es así. Además, tenemos que comprobar el tema de los registros que nosotros estamos utilizando, porque si escribiéramos sobre ellos la desencriptación no funcionaría y el programa con toda probabilidad se colgaría al ejecutarse.

Una forma - y pueden servir muchas, esto siempre es a discrección del programador - de controlar aquello sobre lo que escribimos, sería sencillamente mantener un byte de datos en el que cada bit hiciera referencia a un registro (mejor en el mismo órden de la tabla anterior dado que es estándar y podremos aplicarlo en muchos lugares). Por ejemplo, si el valor de ese byte de datos fuera 01011010 y hubiéramos decidido que un 1 significa "ocupado", entonces estarían "ocupados" los registros ECX, EBX, ESP y ESI... al realizar nuestras rutinas que manejaran registros comprobaríamos este byte de datos para ver si están a 1, en cuyo caso buscaríamos otro registro sobre el que operar (un buen método para consultar este byte, serían las instrucciones que operan a nivel de bit, como BTS, BTEST, etc)

El número de instrucciones que podemos generar como código basura es casi interminable, aunque también hemos de tener cuidado dado que los antivirus suelen cazar estos engines entre otras cosas dado que en el afán por generar código aleatorio se ponen instrucciones "raras", que normalmente no son generadas por un compilador. En cualquier caso, algunos ejemplos de lo que podemos hacer:

- Movimientos (MOV) de valores inmediatos a registros no utilizados, o de registro a registro (tan sólo es importante si el registro destino está ocupado).

- Operaciones aritméticas o lógicas sobre aquellos registros que no utilicemos, tanto con otros registros como con valores inmediatos (ADD, SUB, OR, XOR...).

- Comprobaciones (como CMP o TEST), que en caso de poder ser evaluadas a priori podrían dar lugar a saltos condicionales (JZ, JNZ, etc etc).

- Llamadas a subrutinas (CALL/RET), manejando internamente los puntos de retorno para que el código sea coherente y no de problemas..

- Saltos condicionales y no condicionales (manteniendo un cálculo de hacia dónde se dirigen, como es lógico).

- Otras muchas más instrucciones, como operaciones de cadena tipo LODSB, instrucciones monolíticas como CDQ, STI, CLD, LAHF, etc, de bit como BTEST y demás...

- Lecturas y escrituras de memoria en modelos más complejos en que controlemos estas lecturas y escrituras para no generar fallos de página (por ejemplo, creando un marco de pila en el código y escribiendo allí).

 

Quizá el mejor arma que podemos tener cuando nos embarcamos en la compleja tarea de escribir un engine polimórfico, sea una buena tabla de instrucciones relacionadas con opcodes; personalmente uso los propios manuales de Intel, donde en tres o cuatro tablas se indica la forma de codificar toda referencia a registro e inmediato y sus combinaciones... siempre se puede descargar una copia en PDF en la página de Intel actualizado al procesador más actual - tened en cuenta que hay instrucciones que no servirán para ordenadores antiguos, y siempre es bueno mantener cierta compatibilidad hacia atrás -. Combinando estas tablas (las que hacen referencia a las formas de direccionamiento ModR/M y SIB) con la referencia que en cada instrucción da Intel a nivel de opcode y nuestra propia experiencia, podemos llevar adelante la programación de un generador de instrucciones aleatorias.

 

 

9.2.3.- ¿Puede ser infalible el polimorfismo?

La respuesta, lamentablemente es sencilla. No, no ha existido un engine polimórfico que haga indetectable a un virus, ni lo habrá siguiendo por el camino clásico, por el camino que he mostrado en los dos anteriores apartados. Los antivirus utilizan diversas técnicas, como emular el código para desencriptar el virus; sí, podemos poner trampas anti-debugging en él, pero no es una buena garantía para ese método en particular. Y en cualquier caso, el verdadero problema no está ahí.

Sería largo de explicar y tendría que tocar teoría de lenguajes y autómatas, pero sería sencillo demostrar que un engine polimórfico no puede llegar a ser indetectable. La generación de código aleatorio y las instrucciones de desencriptado equivalen a un alfabeto que utilizamos para hacer "palabras", palabras que son el código que hemos fabricado de forma semi-aleatoria. Lamentablemente, ese código generado ha sido realizado respecto a unas reglas de producción - no importa que las llamadas a funciones generadoras se intercalen de forma aleatoria -, y ese hecho lleva a una conclusión propia ala teoría de lenguajes y autómatas; que todo lo que generemos es detectable, o, en términos de esta teoría, que toda producción tiene un autómata correspondiente que lo detecta sin errores.

Es algo que sin duda no puedo explicar en un sólo párrafo; quien esté más interesado que mire cierto artículo que escribí hace un par de años llamado "Polimorphism and Grammars" bajo el seudónimo de Qozah demostrando el hecho de que, lamentablemente, un engine polimórfico no puede hacerse indetectable.

Podemos sin embargo adoptar otras estrategias; apunté una en aquel entonces que puede hacer a un virus inlimpiable si es trabajada con suficiente intensidad (técnica que utilizó de forma primitiva en mi virus Unreal)... no obstante no ofrecía soluciones al problema de la indetectabilidad. Lamentablemente, a estas alturas seguimos sin tener ninguna.

 

 

9.2.4.- Otras formas de aproximarse al polimorfismo

El paradigma clásico del polimorfismo es el que he intentado mostrar en los apartados anteriores; un "centro coordinador" que llame aleatoriamente a rutinas que generan instrucciones basura, mezclado con código que genera instrucciones válidas dedicadas a desencriptar el virus y las cuela entre medias del código basura.

Sabemos ya que este sistema no puede hacer indetectable un virus, aunque sin duda puede hacer mucho más difícil su detección si se lleva a cabo de forma correcta. ¿Qué alternativas nos quedan por probar? He aquí algunas ideas:

- Paralelización masiva débilmente coordinada: El nuevo paradigma de la IA, la "embodied cognitive science", puede ponernos en la pista sobre la realización de engines polimórficos más efectivos. Esta, se basa en la acumulación de multitud de procesos sencillos que dan lugar a una conducta que puede ser calificada como inteligente; pero esta conducta no está preparada y es difícil de predecir pues se basa en la interacción del agente con su entorno y la interacción de todos sus procesos. Un camino pues interesante en la programación de engines polimórficos consiste en basar el código en una paralelización masiva de rutinas que no sólo generen código sino que lo modifiquen de diversas maneras. La interacción de estos procesos podría dar lugar a resultados no previsibles de tal modo que no fueran detectables... no obstante, hay que tener en cuenta que no podemos dejar a su libre albedrío a estos procesos (el código final tiene que ser ejecutable sin problemas para el fichero infectado y para el virus), y que al fin y al cabo cuando ejecutamos este tipo de modificaciones seguimos ejecutando código de forma secuencial por mucho paralelismo que haya.

Por un lado deberíamos anular coherentemente las restricciones clásicas de la programación concurrente sobre secciones críticas; normalmente cuando dos procesos en paralelo acceden a la misma porción de datos nunca se deja que accedan a la vez pues podrían crear un estado incongruente en los datos. Por ejemplo, si concurrentemente se ejecutan z = z+1 y z = z+2, hacemos que solo uno de los dos pueda acceder a "z" en el mismo momento, pues sino en lugar de ser el resultado final un "z = z + 3", podría ser "z = z+1" o "z = z+2" (si el proceso A carga z en un registro en su espacio de ejecución, después lo hace B, modifican el valor de z en estos registros y vuelven a ponerlos en memoria, las sumas no se han añadido entre sí sino que sólo uno de los resultados se colocará en memoria).

La cuestión, es que si utilizamos estas mismas restricciones que sirven normalmente en la programación concurrente para no generar incongruencias, resulta prácticamente como si estuvieramos ejecutando código secuencial y predecible... tendríamos que programar de modo que los resultados no fueran adivinables y por tanto detectables, pero estando a la vez pendientes de que los resultados que se generen aún siendo imprevistos sigan siendo congruentes y ejecutables, sin crear ningún problema para el virus ni para el ejecutable infectado. Una tarea nada sencilla...

- Generación de código que intente imitar código legítimo: Esto es una variante bastante interesante dentro del objetivo de la mayoría de los engines polimórficos, y que ya está siendo adoptada por algunos programadores de virus. Utilizando el método clásico quizá el resultado sea una serie de instrucciones que, si bien son bastante aleatorias, no son realmente coherentes como código. Con esta aproximación, se pretende que el código resultante sea realmente parecido a un programa legítimo.

Aquí entra en buen grado la investigación individual; podríamos por ejemplo generar inicios de subrutina con un aspecto tipo MOV EBP,ESP/SUB ESP, XX, que suele ser el comienzo de toda subrutina e incluso de buena parte de los programas que veamos; se trata de una estructura que se llama marco de pila y que consiste en que al llamarse a una subrutina se deja en la pila un espacio (al inicio del cual apunta EBP) para almacenar las variables locales que se utilizarán en ese procedimiento (de modo que no se gasta espacio en memoria inútilmente). Así, si por ejemplo en el código C con que fue escrito un programa tenemos tan sólo una variable tipo "long int variable", normalmente el compilador al principio de ese procedimiento restará 4 a la pila (4 bytes es el tamaño de un long int), y con EBP hará referencia a esta variable (y a otras en caso de haber más).

La imitación de este tipo de estructuras comunes generadas por compiladores de lenguajes de alto nivel, puede dar un carácter muy interesante a un engine polimórfico que ya no simplemente pretenda "ser muy aleatorio", sino parecerse lo más posible a código legítimo hasta un punto óptimo en que sería indetectable al no poder distinguirse de código real.

El problema de esta técnica suponiendo el caso ideal en que hubieramos controlado una coherencia de modo que el código pareciera realista, es el hecho de que aun así tenemos que insertar en este código basura las instrucciones que desencriptan el virus; así pues, aunque hubiéramos conseguido hacernos indistinguibles del código legítimo de cualquier otro programa, seguiríamos teniendo el problema de que unas pocas instrucciones en ese código, las que desencriptan nuestro virus, no son variables del mismo modo y por tanto puede fabricarse un programa - autómata si volvemos a la teoría de lenguajes - que detecte el autorreplicante.

Se trata, en cualquier caso, de un camino interesante a explorar y que se lo pone mucho más difícil a quien intente detectar nuestro virus; tendremos que centrar entonces mucho esfuerzo en hacer que las instrucciones críticas, las que desencriptan nuestro virus, se oculten de la mejor forma posible (por ejemplo, Mental Driller sostiene con mucha lógica que se debe evitar desencriptar linealmente el virus para evitar levantar sospechas, es decir, no basarlo en un xor [esi]/inc esi sino en una forma más compleja de ir recorriendo todo el virus.

 

 

9.2.5.- Conclusiones

La programación de un engine polimórfico no es precisamente una labor sencilla. Requiere mucho esfuerzo, mucha imaginación y planificación para que nada falle; si cometemos errores su consecuencia será probablemente el que nuestro virus deje de funcionar en aquellos ficheros infectados a los que nuestro error en la generación de código afectó.

Sin embargo, no he dado código más que para ilustrar pequeñas cosas; pero lo hago porque creo que la programación de un engine polimórfico, a la par que una de las cosas más difíciles de hacer cuando se lleva a cabo en serio, es de las más personales y gratificantes con las que podemos atrevernos escribiendo un autorreplicante. He pretendido hacer que se entienda como funcionan; con esto en la cabeza, la mejor opción es programar uno desde cero con ideas propias. Es posible que se descubra o se utilice algo que a nadie se le había ocurrido aún.

No obstante, como he insistido en el punto anterior, no debemos ser demasiado optimistas respecto a las posibilidades de esta técnica; de momento, nadie ha descubierto como fabricar el virus indetectable.

Black Hole  
   
Facebook botón-like  
 
 
Hoy hay 28 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