Infectando PE 0.0 Nota del autor Voy a seros sinceros; lo mio no es la programación y mi idea era escribir un documento sobre "supercomputación", pero la cosa tendrá que esperar por motivos muy ajenos a mi voluntad. Así que buscando ideas, me comentaron (gracias míster xD) que podía escribir algo sobre virii (tema que realmente casi no he tocado, pero hace medio añito escribí un infector de ejecutables para ms-dos), por lo que he intentado desempolvar las estanterías que tengo /vacias/ dentro de la cabecita. Y este es el resultado. Los requisitos mínimos son muy simples; solo pido ganas de aprender informática, y algunos conocimientos mínimos de ensamblador, porque no me pondré a explicar lo que es un "push". Si careces de estos conocimientos, busca algún documento en google antes de ponerte con este documento. Sería como querer modificar un exploit sin saber C. 1.0 Por donde empezar Primero pensaba que lo mejor para que os quedarais con el tema (y que no pensarais que esto es el típico texto
de virus en VBA :P) era empezar soltando un rollo digno de cualquier profesor de secundaria sobre la estructura
de los ficheros ejecutables de windows, pero he decidido empezar explicando como hacer para lograr que nuestro
codigo en ensamblador sea portable al máximo, incluso dentro de una misma máquina. Me explicaré. Cuando programamos
en ensamblador (o en cualquier lenguaje) el compilador/ensamblador traduce nuestro codigo a lenguaje de máquina,
treduciendo las llamadas a registros por su dirección de memoria física en el sistema actual. Para solucionar este problema algun coder inventó el "Delta Offset". Esto es una rutina que calcula el desplazamiento
del ejecutable actual en memoria. La rutina es muy sencilla. Lo único que hace es meter la posición de memoria actual
en un registro, y restarle la posición de memoria inicial. La posición actual la obtenemos con un CALL, porque como
recordareis, lo que realmente hace un call es empujar a la pila la dirección de ejecución siguiente al él, y saltar
a la posición que indica la etiqueta, y la posición inicial ya la tenemos "codificada" por el ensamblador, que nos
cambio la etiqueta a la que llamamos con el call por su posición en la memoria original. call CalculoDelta CalculoDelta: pop ebp sub ebp,Offset CalculoDelta Para aquellos vagos que no han mirado nada de ensamblador antes de empezar con esto, voy a explicar el codigo paso a paso (step by step). Primero llamamos a CalculoDelta, y como decimos esto se podría traducir en push dirección_memoria_siguiente jmp CalculoDelta Por lo que en la última posición de la pila (stack) tenemos la dirección de memoria inicial del ejecutable actual.
Lo siguiente que hacemos es "pop ebp" (si no sabeis lo que hace la instrucción pop, mal andamos :P), que saca de
la pila la última posición y la mete en el registro que se le pasa como parametro (en este caso ebx, por lo que
ahora en ebp tendríamos la dirección de memoria actual). Pero como lo que nosotros queremos es calcular el
desplazamiento, y no la posición actual, le tenemos que restar la dirección inicial, que sacamos de CalculoDelta.
Lo que hacemos con "sub ebp,Offset CalculoDelta" es calcular el Delta Offset (sub)strayendo (toma regla pnemotécnica
para acordarte de la instrucción xD) la dirección de memoria que teniamos la primera vez con la dirección de memoria
que tenemos ahora, ¡así que en el registro ebp tenemos el preciado desplazamiento! Vuelvo a avisar, esto no intenta ser una classe de programación de virus para infectar el mundo y colgar internet xD, simplemente es un doc para aprender fundamentos de programación de este tipo de programas. Si teneis un antivirus en marcha y ejecutais este codigo, va a saltar, porque que un ejecutable empiece con este codigo es muy sospechoso, ¿no creeis? 2.0 Buscando medios para trabajar Ahora que tenemos el desplazamiento dominado, tenemos que empezar a escribir codigo de verdad, ¿o no? Para esto necesitamos usar API's de Windows, que son muy sencillas de usar. Simplemente tenemos que empujar una serie de valores para decirle al SO como queremos que se ejecute la función y llamar a con un call. La gran mayoría de funciones que usaremos (por no decir todas) están incluidas en una DLL del sistema llamada Kernel32.dll. El problema con el que topamos es el de siempre (o almenos el mísmo por el que calculamos el delta offset), y es que queremos que nuestro "virus" se ejecute en el máximo de SOs posibles. Y como habreis imaginado la dll de la que hablamos no está en la misma posición de memoria entre distintos SOs (incluso no está en la misma posición de memoria entre distintos Service Packs de una misma versión de Windows). Asi que nos encontramos con la misma batalla que antes. No podemos compilar una llamada directamente a la API, sino que antes de llamar tenemos que encontrar donde está situada la dll (kernel32.dll) en la memoria, y una vez con la posición de memoria en la mano tenemos que mirar dónde esta la dirección física de la función a la que queremos llamar. La manera más sencilla de localizar la dirección de Kernel32.dll la he leido por ahi. Como la del delta offset, es una rutina muy común en los virus (y por tanto cualquier antivirus decente la va a detectar), pero nosotros solo queremos aprender. Vamos a pensar un poco. En Kernel32 tenemos una función llamada CreateProcess, que se usa siempre que ejecutamos un programa. Y como nuestro virus es un programa, y ha sido "creado" con esa función, que a su vez a sido ejecutada por un "CALL", ya tenemos la solución. Volveré a explicarme ;). Un CALL, como ya hemos dicho, empuja al último registro de la pila la posición actual dentro de la memoria física (que corresponde al registro EIP, miraros un poco de fundamentos de computación). Asi que la dirección de retorno estará en en el registro SS:ESP (SS es el registro de segmento de pila, por lo la pila está en SS:ESP, ya que esp es el registro de pila, que indica la dirección a la que esta apunta). Pero si le decimos SS:ESP cogeriamos parametros pasados al programa, y ignorando lo que no nos interesa sabemos que la dirección está en "SS:[ESP+8h]". Pero no exactamente, aún podemos encontrar más basura delante, pero como las DLLs también son PEs y, como veremos más adelante, estos empiezan con la cadena MZ, así que iremos haciendo un bucle que vaya cambiando la posición de memoria y comparando con los dos bytes MZ, hasta que encontremos exactamente el inicio de la Kernel32.dll :) mov eax,ss:[esp+8h] and eax,0FFFF0000h BuscarInicio: sub eax,10000h cmp word ptr [eax],'MZ' jnz BuscarInicio Primero sacamos la posición de la función CreateProcess dentro de la API Kernel32. Lo que hacemos con la instrucción AND es "borrar" las últimos ctres cifras de nuestra dirección de memoria, para trabajar con números "exactos". Ahora, le restamos 10000 a esta dirección y miramos si ya tenemos a MZ. Si no (Jump Not Zero, no?) volvemos a intentarlo, una y otra vez hasta que encontremos el inicio. Bendito sea Tux, ya tenemos nuestra dirección de memoria de Kernel32; pero ahora tenemos que encontrar la dirección de las funciones que necesitemos. No es desanimeis, que hay una función que nos hace la vida muy sencilla : GetProcAddress. Esta función nos dará las direcciones que buscamos, pero no sé si ya lo habeis pensado ... ¿como sacamos la dirección de esta función? ([OFFTOPIC]¿Que fué primero, el huevo o la gallina? xDD[/OFFTOPIC] Después de esta interesante aventura para sacar la dirección de memoria de la API, toca asentar adrenalina con un poco de teoría sobre la estructura de los ejecutables, para depués poder encontrar la preciada función "GetProcAddress" ;). 3.0 Acercandose al enemigo (PE) El PE (Portable Ejecutable, ¿os habeis quedado con el nombre?) es un formato de archivo
ejecutable que introdujo Microsoft en Windows NT 3.1 (nota de Linuxero empedernido : he
leido por allí que lo hicieron basandose en un formato propio de Unix, llamado COFF). *Era coña, no quiero el típico aluvión de críticas por parte de Windowzer0s negando la evidencia (que se equivocaron en el método). Creo que queda claro que admiro algún software de Redmon (técnicamente hablando, algunos de vosotros ya sabeis que espero impaciente las "innovaciones" de Longhorn**), pero admiro muchísimo más otro software (2.6.10 rules). **http://mnm.uib.es/gallir/posts/2005/01/01/62/ 3.1 Estructura básica del PE Para tener una idea general de la forma que tiene el PE, os voy a pegar el típico diagrama sobre su estructura, para después pasar a explicar por encima cada parte. --------------------- | Cabecera MZ | |---------------------| | DOS Stub | |---------------------| | Cabecera PE | |---------------------| | Opcional | |---------------------| | | | DATOS | | | |---------------------| | TABLA DE SECCIONES | |---------------------| | | | SECCIONES | | | --------------------- Cabecera MZ : O lo que es lo mismo, dos bytes (los caracteres MZ), la cabecera de los antiguos ejecutables. Marca el inicio de todo PE. Dos Stub : Es un simple programa que, si es iniciado en modo MS-DOS, dice que no puede ser ejecutado de esta manera (The program cannot be run in DOS mode). Cabecera PE : Empieza con una firma de 4 bytes, los caracteres PE seguidos de dos nulos, que nos indican que empieza "la fiesta". Después de esto tenemos la cabecera del fichero objeto (object file header), que contiene información sobre el ejecutable. Si abrimos un PE con un editor hexadecimal, veremos que después de la MZ viene este cabecera, que reconoceremos porque como ya hemos dicho empieza con estos 4 bytes : PE\0\0. Cabecera Opcional : Opcional en el sentido de que no es requerida por los ficheros. Tiene dos partes, pero no entraremos en más detalles. Directorios de datos : Simplemente un almacen de información. Secciones varias : Esto pueden ser secciones de pila, de datos, de codigo ... Navegando por la red he encontrado algo más "profesional" que mi intento de diagrama anterior :P Con la estructura básica de un PE más o menos en la cabeza, retomemos la emocionante busqueda de nuestras direcciones de memoria, jejeje 4.0 Encontrando funciones dentro de la API Anteriormente conseguimos la posición en memoria (que ahora está guardada en eax) de la api de dónde
tenemos que sacar todas las fucniones que usemos. Una vez encontrada tenemos que usar la función
GetProcAddress, que a su vez también tiene que ser encontrada antes de poder ser usada ;). Eso
es lo que explicaremos en este punto, como encontrar la preciada función y como usarla para encontrar
las direcciónes de las funciones que necesitemos. En un diagrama podría quedar algo así : MZ 03eh -> Cabecera PE 78h -> Sección de exportaciones AddressOfNames -> Todos los nombres de las funciones que buscamos Escrito en ensamblador de una forma muy primitiva y poco optimizada (pero que se entiende perfectamente, y ese es el objetivo) quedaría así : mov edi, eax ; eax=base del kernel xor eax,eax mov eax, dword ptr ds:[edi+03ch] add eax, edi ; añadimos edi por lo del enlaçe a la cabecera del PE, recordemos mov esi, dword ptr ds:[esi+078h] add esi, edi ; ahora ya tenemos la sección de exportaciones mov edx, dword ptr ds:[esi+020h] ; con esto entramos en AddresOfNames, para buscar una entrada que se ajuste a la función que buscamos xor ecx, ecx Bucle: mov eax, dword ptr ds:[edx] add eax, edi cmp dword ptr ds:[eax],'PteG' jnz NoLoEs cmp dword ptr ds:[eax+4h],'Acor' jnz NoLoEs cmp dword ptr ds:[eax+8h],'erdd' jnz NoLoEs jmp Cojonudo ; Llegamos a NoLoEs si el nombre no coincide NoLoEs: add edx,4h ; para apuntar al siguiente de la lista inc ecx jmp Bucle Cojonudo: <continuamos el codigo> (*) Lo que hace el codigo anterior es ir saltando de enlaçe en enlaçe (y tiro porque me toca .. :P) por dentro
del ejecutable, y una vez dentro de la sección de exportaciones se va a la AddressOfNames (añadiendo 20h
al inicio de la seccion; mov edx, dword ptr ds:[esi+020h]) y va buscando GetProcAddres (comparando cada
entrada con ella, y si la entrada que acaba de comparar no lo és, incrementa el registro ecx, que actua
como contador). ; Tenemos en ECX el numero de desplazamientos del bloque de antes. rol ecx,1h mov edx,dword ptr ds:[esi+24h] ;AddressOfNameOrdinals add edx,edi ; edi = base del kernel add edx,ecx movzx ecx,dword ptr ds:[edx] mov edx,dword ptr ds:[esi+01ch] add edx,edi rol ecx,2h ; * 4 add edx,ecx mov eax,dword ptr ds:[edx] add eax,edi ; Ajustamos a la base kernel Con rol multiplicamos, luego (en edx) hacemos la suma de "base de Kernel32.dll + posición de memoria de AddressOfOrdinals + posición de GetProcAddress dentro de AddressOfNames". Ahora toca hacer la segunda suma, que es "posición de AddressOfFunctions + base del kernel + Ordinal de GetProcAddress * 4". ¡Ya tenemos la posición de memoria de GetProcAddress en eax! ¡Misión cumplida! 4.1 Encontrando funciones con GetProcAddress Como dijimos, en lugar de hacer toda esa vuelta cada vez que queremos llamar a una función, basta que usemos este codigo escrito por Wintermute, que usa GetProcAddress para sacar la dirección de memoria de la función que le indiquemos. ; Tenemos en ECX el numero de desplazamientos del bloque de antes. mov dword ptr ds:[GPAddress+ebp],eax push offset direccion + ebp ; direccion del nombre de la función push edi ; La dirección del kernel call GetProcAddress GPAddress equ $-4 <codigo> direccion: db 'FuncionQueQuiero',0 ; el ,0 es importante :) Dicho esto, se da por finalizado el primer doc sobre virii de la ezine de hispabyte, con toda
la base teorica y práctica necesaria para seguir el próximo doc (la secuela de este, que espero
que sea la excepción que confirme la regla de eso de que segundas partes nunca fueron buenas),
en el que explicaremos como podemos encontrar PE para infectar, y lo principal, como infectarlos.
También intentaré pegar algun codigo completo y compilable, con fines educativos, por supuesto. 5.0 Bibliografía y enlaces Curso de Virus Wintermute -->
http://www.13t.org/wintah/virus/curso/ (*) El codigo ensamblador está "integramente" copiado del Curso de virus de Wintermute, aunque con algunos cambios para que se ajuste a mi explicación y al codigo añadido. servomac (
servomac@gmail.com ) -
http://servo.blogia.com/
|