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. Por ejemplo, si hacemos un mov ebx,[variable] lo que hace es "traducir" variable por la posición de memoria donde se encuentra esta, así que en lugar de buscar donde se encuentra cada vez, el ejecutable ya asignará la posición de memoria N a variable, por ejemplo 0BFFF00h. Esto está muy bien, pero a la hora de copiar el virus a otro ejecutable nos supone un problema, ya que este no esta situado en la misma posición de memoria que el ejecutable que nosotros compilamos. Así que en la dirección de memoria 0BFFF00h solo encontraremos la variable en la "primera generación", en la segunda (y posteriores) encontraremos cualquier otra cosa. 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. Explicado puede parecer muy lioso, por lo que nada mejor que pegar un poco de codigo:
    	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! Una vez que tenemos esto simplemente se lo sumamos a cada dato al que queramos acceder. Así corregimos el cambio de posición en memoria que ha sufrido el ejecutable desde que lo compilamos por primera vez. 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). Una de las principales puertas que nos dejaron abiertas (luego os explicaré exactamente porque) fué la de perseguir la retrocompatibilidad (poder ejecutar aplicaciones escritas para versiones anteriores del SO). La retrocompatibilidad es un gran logro, no lo niego, sino que además creo que és necesaria, pero creo que la lograron de una forma muy "basta" (esa gente no hubiera aprovado una práctica de programación*). Para ello, al inicio de la estructura del PE mantuvieron una primera cabecera conocida como MZ o cabecera DOS, propia de MS-DOS, que nos permite identificar el inicio de un PE. Además, las otras cabeceras también nos revelan mucha información sobre el ejecutable en si. *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 http://www.madchat.org/vxdevl/papers/winsys/pefile/peheader2.jpg 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. Para ello tenemos que volver a la explicación anterior de la estructura de un PE, porque sabemos que en una de las secciones encontraremos una lista de las funciones, entre ellas GetProcAddress, pero para acceder a esta lista de funciones tenemos que localizar las secciones dentro de la estructura del PE. En eax tenemos el inicio de Kernel32.dll, que al ser un portable ejecutable empieza con la típica cadena MZ. Pero si a la dirección de inicio le sumamos 03eh (en hexadecimal), encontramos un "enlaçe" a la cabecera real del PE (saltandonos el Dos Stub). Una vez en la cabecera PE, hacemos lo mismo, le añadimos 78h (también en hexadecimal) a la dirección y encontramos otro enlaçe, esta vez a la sección de exportaciones dónde encontramos otro enlaçe a listados de nombres con funciones y tal. 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). A simple vista parece que hemos perdido el tiempo, porque ¿para que me sirve a mi saber en que posición de la lista de AddressOfNames, si yo lo que quiero es saber su dirección de memória? La solución está otra de las secciones de exportaciones, AddresOfOrdinals. Realmente no sé el porque, pero siguiendo un procedimiento raro podemos sacar la dirección de memoria de la función GetProcAddress. Realmente, la posición 1 de la sección AddressOfNames está en la posición 2 de AddressOfOrdinals (siempre el doble que en la posición de AddressOfNames). Así que cogemos la posición dónde encontramos la cadena GetProcAddress dentro de AddressOfNames (que tenemos guardado en el registro ecx) y lo multiplicamos por dos, y esto le sumamos la posición de memoria del principio del registro AddressOfOrdinals (que es lo mísmo que hicimos para encontrar el AddressOfNames, pero en lugar de sumarle 20h a esi, le sumamos 24h porque está justo después de él en la sección de exportaciones). Haciendo esto obtendremos el "Ordinal de la Función". Ahora si, este número lo multiplicamos por 4 (esto parece una classe de primaria, tanto multiplicar xD) y ya tendremos la posición (dentro de AddressOfFunctions) del enlaçe a GetProcAddress. Perfecto!!! Vamos a ver como sería el codigo ..
    ; 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. ¡Os espero en la próxima ezine! 5.0 Bibliografía y enlaces Curso de Virus Wintermute --> http://www.13t.org/wintah/virus/curso/ Intro to Win32 Programing in ASM, and PE infection --> http://vx.netlux.org/lib/static/vdat/tuiwin32.htm PE structure --> http://kalbasa.paradoxinc.org/coding/bin/htm/pe.htm (*) 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/