ESTUDIO COLECTIVO DE DESPROTECCIONES
Última Actualizacion: 25/10/2001

Programa Inserción de código en VB W95 / W98 / NT
Descripción Importación de funciones en VB usando DllFunctionCall
Tipo  
Tipo de Tutorial [X]Original, []Adaptación, []Aplicación, []Traducción
Url http://kickme.to/wkt
Protección  
Dificultad 1) Principiante, 2) Amateur, 3) Aficionado, 4) Profesional, 5) Especialista
Herramientas Hiew, W32Dasm. Opcionalmente SoftIce y Topo.
Objetivo Importación de funciones para la inserción de código en VB
Cracker Mr.Blue
Grupo Whiskey Kon Tekila
Fecha 6 de Agosto de 2.000

INTRODUCCION

¿Cómo podríamos definir la ingeniería inversa?

Si queremos definirla de manera "académica" debemos antes acotar el significado de la ingeniería convencional. Por ingeniería podemos entender todas aquellas actividades destinadas al desarrollo de algún tipo de "mecanismo" que presente un comportamiento determinado deseado. Desarrollaremos al engendro capaz de presentar y reproducir dicho comportamiento...

Visto esto, la ingeniería inversa perseguirá analizar el "mecanismo" en cuestión para determinar cómo consigue llevar a cabo los comportamientos que presenta, o dicho en otras palabras, saber cómo funciona.... y ya sabemos que en nuestra actual sociedad de la (des)información, "el saber es poder"... ;-)

El fin último de la ingeniería inversa es obtener de "algo" desarrollado por otros, los conocimientos necesarios y suficientes sobre su funcionamiento como para podernos construir nuestro "algo" propio, que copie en parte (o en todo) el funcionamiento del "engendro" original. Si llegar a estos extremos de creación de un "clónico" o "copia" del mecanismo original, la ingeniería inversa también nos permitirá modificar en cierta medida el comportamiento original del objetivo.

Aplicado al mundo informático, Estado+Porcino en su primer capítulo, definió "crackear" como "... el arte de reventar protecciones software/hardware con fines intelectuales, personales pero no lucrativos. Crackear también se llama ingeniería inversa (Reverse Engineering), ya que sin el programa fuente se es capaz de analizar el comportamiento del programa y modificarlo para tus intereses."

En esta modificación del comportamiento podemos, desde limitarnos a cambiar alguna de las funcionalidades del programa hasta incluso llegar a añadir nuevas funcionalidades.

La adición de nueva funcionalidad a un programa generalmente solo podrá realizarse añadiendo código propio al ya existente, que implemente dichas novedades. Por otro lado, para cambiar alguna funcionalidad presente en el programa puede bastar con modificar algunas de las instrucciones del mismo, sin necesidad de añadir código nuevo, aunque no necesariamente, ya que la inserción de código nuevo también puede cambiar características ya existentes.

Ejemplo de inserción de código para la implementación de nuevas funcionalidades se puede encontrar en el E+P Cap. 8. En ese tutorial se muestra en toda su grandeza las posibilidades que abre la inserción de código frente a la simple modificación, inserción de nuevo código que por otro lado es la única solución posible ya que la funcionalidad deseada no se encontraba implementada en la víctima.

Por otro lado, Mr.Crimson es su tutorial sobre Cracking Multitarea! muestra otra forma de realizar un parcheado en memoria sobre un objetivo empaquetado sin necesidad del clásico cargador. Para ello, inserta nuevo código en el programa objetivo para crear un hilo que, ejecutándose de forma paralela al desempaquetador, espera a que éste finalice su tarea antes de realizar las modificaciones. Es un ejemplo de modificación de una de las funcionalidades del programa (la desagradable funcionalidad de los shareware de "no funcionar" a tope si no estás registrado) mediante la inserción de nuevo código. Para aprender más sobre el tema consulta el tutorial sobre el Topo de Mr.Crimson.

Como se puede ver, la inserción de código nuevo en un programa abre nuevas vías, que en algunos casos puede convertirse en la única vía para lograr nuestros objetivos.

IMPORTACION DE NUEVAS FUNCIONES EN VB

A la hora de insertar nuevo código en un ejecutable, nos puede interesar no aumentar el tamaño del mismo. Para ello el nuevo código debe insertarse en aquellas zonas del ejecutable que no sean utilizadas por el mismo. Estas zonas suelen ser utilizadas por los compiladores para alinear cada una de las secciones, y suelen tener sus bytes a cero.

¿Para qué queremos importar nuevas funciones? Los motivos pueden ser dos, o bien necesitamos utilizar alguna función de la API o de cualquier otra DLL que no es importada por el programa o bien no tenemos suficiente sitio libre en el ejecutable para colocar todo el código que deseamos insertar por lo que podemos usar la opción de crear una DLL con la nueva funcionalidad para después usarla desde el ejecutable.

Tal y como se mostró en el tutorial Cracking Multitarea! de Mr.Crimson, la importación de funciones de librería se realiza mediante llamadas a las funciones GetModuleHandle o LoadLibrary para cargar la DLL y GetProcAddress para obtener la dirección de la función que nos interesa.

Estas funciones suelen estar ya importadas en todos los ejecutables, por lo que están disponibles para la carga de nuevas funciones en tiempo de ejecución.

El problema se plantea en aplicaciones desarrolladas con Visual Basic. Las aplicaciones creadas bajo este entorno de Microsoft usan para su funcionalidad básica exclusivamente funciones de la librería de Visual Basic: MSVBVM50.DLL para VB5 y MSVBVM60.DLL para VB6. Por funcionalidad básica se entiende la creación de ventanas, la creación de hilos, el procesado de mensajes, interfaz con el usuario, acceso al registro de Windows, comparaciones de cadenas,... en resumen, la mayor parte de las funciones básicas de la API son invocadas por las funciones de las librerías de VB, casi nunca son invocadas desde el propio ejecutable.

Y alguien se preguntará... ¿y por qué no utilizamos las funciones de las librerías de Visual Basic?. El motivo no es otro que la complejidad de uso de dichas funciones. No existe ninguna documentación sobre las mismas, sí sobre las funciones que se invocan desde el código fuente de VB pero no sobre las funciones que las implementan en las librerías. En algunos casos es fácil identificar qué función de la librería es la que implementa una determinada función invocada desde Visual Basic, pero en muchos casos el paso de parámetros a la función de la librería no es tal y como nos lo esperamos según los parámetros que se documentan en la ayuda de Visual Basic.

En algunos casos sencillos puede ser posible usar directamente las funciones de la librería de Visual Basic, pero en gran parte de los casos puede que no seamos capaces de convertir a funciones de la librería de VB el código que deseamos insertar, codificado con funciones estándar de la API de Windows.

DLLFUNCTIONCALL

Tal y como ocurre con casi todas las demás funciones de la API, GetProcAddress y GetModuleHandle no son importadas por el ejecutable de Visual Basic, si no que se invocan desde la librería de VB. ¿Y cómo lo hacemos para importar las API's que necesitamos si no disponemos de GetProcAddress y GetModuleHandle? Tranquilos...

La función de la librería de VB encargada de cargar nuevas funciones en tiempo de ejecución es 'DllFunctionCall'. Esta función está presente en la tabla de importaciones de cualquier programa de Visual Basic que utilice funciones de librerías externas ajenas a las librería de VB, permitiendo cargarlas en tiempo de ejecución.

En este ejemplo se muestra una sencilla aplicación en VB con un esquema de protección basado en el Registro deWindows y que utiliza directamente la función de la API "MessageBox", en lugar de "MsgBox" que es la sustituta en Visual Basic. Esto provocará que el compilador incluya el código necesario en el ejecutable para cargar dicha función, pertenenciente a 'User32.dll', en tiempo de ejecución mediante 'DllFunctionCall'.

Colocando un "bpx DllFunctionCall" en el SoftIce y examinando el parámetro que se le pasa a esta función a través de la pila podemos sacar qué argumentos son los que utiliza y qué es lo que devuelve:

DWORD DllFunctionCall( // Devuelve la dirección de la función
DWORD lpestructura ); // Puntero a estructura de datos

La estructura sigue la siguiente plantilla:

LPSTR lpmodulo, // Puntero a cadena con el nombre del módulo
LPSTR lpfuncion, // Puntero a cadena con el nombre de la función
DWORD imagebase, // Base de la imagen del ejecutable
DWORD lpresultado // Puntero a buffer para almacenar resultados

A partir de la dirección indicada por 'lpresultado' la función alamacena tres DWORD's. En las pruebas realizadas los datos devueltos han sido los siguientes:

DWORD 0 // Valor cero.
DWORD imagebase // Base de la imagen de la DLL (= handle de la DLL)

DWORD direccion

// Dirección de la función

Tras la ejecución de la función, la dirección de la función solicitada se encuentra tanto en el registro EAX como almacenada en la DWORD apuntada por (lpresultado+8).

Si traceamos a 'DllFunctionCall' veremos como invoca a las funciones GetModuleHandle (o LoadLibrary si la librería no se encuentra cargada) y GetProcAddress.

LA INSERCION DE CODIGO COMO ALTERNATIVA

La protección del ejemplo adjunto persigue que solo se pueda ejecutar el programa 3 veces. A partir de ahí mostrará un mensaje de error. El número de veces que se ha ejecutado el programa se almacena en el registro, concretamente en el valor 'Ejecuciones' de la clave 'HKEY_CURRENT_USER\Software\VB and VBA Program Settings\DllFunctionCall\Demo'.

Normalmente cuando nos enfrentamos con una protección de este tipo que reside en el registro de Windows, solemos decubrir que borrando la clave del registro adecuada antes de arrancar la aplicación ésta interpretará que acaba de instalarse, reiniciándose el periodo de evaluación. Este sistema de protección, a pesar de su manifiesta debilidad, es mucho más utilizado de lo que muchos pudieran imaginar. Aplicaciones "insignia" como el FrontPage 2000 y el PhotoDraw 2000 de empresas tan "punteras" como Microsoft usan este esquema de protección.

Cualquiera, con unos conocimientos mínimos de programación, es capaz de desarrollar un pequeño cargador destinado a borrar las claves de registro necesarias para después cargar la aplicación "víctima" con todo el periodo de evaluación por delante...

Otros, más iniciados en este arte, pueden modificar el código de la aplicación para que cuando acceda al registro "piense" que no ha encontrado nada. Para ello deben localizar el punto en el que la aplicación verifica si se localizó la clave en el registro o no. Si forzamos que la aplicación interprete que no lo ha encontrado, a pesar de que está ahí, se volverá a reinicializar el periodo de evaluación.

Localizar este punto puede ser en algunos casos más o menos complejo. Si además la aplicación está desarrollada en VB con la opción de compilación en p-code, esta localización y la posterior modificación puede producir verdaderos dolores de cabeza (ver mi tutorial sobre el Cuentapasos 3.75).

En nuestro ejemplo, si no podemos determinar la localización del código a modificar, ya sea por la complejidad del programa en cuestión o por nuestra limitada "habilidad" con el SoftIce, tan solo nos quedan dos opciones... hacer un cargador como el que se ha descrito que borre la clave del registro o insertar el código necesario en el programa para que al arrancarlo sea él mismo el que borre la clave del registro.

BUSCANDO HOSPEDAJE

Lo primero que debemos hacer a la hora de insertar código es acotar cuánto espacio vamos a necesitar y ver si el ejecutable dispone de una zona libre lo suficientemente grande como para albergar dicho código.

¿Cómo lo localizamos el "agujero" para meter nuestro código? Generalmente los compiladores alinean las distintas secciones, dejando huecos entre ellas que son susceptibles de ser usados para nuestros malvados propósitos. Estos huecos suelen estar rellenados con bytes a cero. En nuestro ejemplo, según los datos que muestra la cabecera PE del ejecutable para la sección de código .text ésta tiene un tamaño en memoria una vez que se ha cargado el ejecutable de 0E8Ch (3724) bytes mientras que el tamaño en el fichero es de 1000h (4096) bytes. Teniendo en cuenta que esta sección comienza en el offset 1000h del fichero, el "agujero" comenzará a partir del offset 1E8Ch (401E8Ch en RVA) y tendrá un tamaño de 4096-3724=372 bytes.

Para ayudarnos a localizar estas zonas disponemos de un pequeña herramienta diseñada por Mr.Crimson específicamente para facilitar la inserción de código en los ejecutables y bautizada con el nombre de Topo. Una de las facilidades que ofrece es la posibilidad de buscar el mayor hueco de bytes presente en un ejecutable, aunque con algunas pequeñas limitaciones explicadas en la ayuda del programa. En nuestro caso la herramienta nos localiza un hueco de 364 bytes a partir de la RVA 401E94h, offset 1E94h del fichero. La diferencia de 8 bytes con respecto a nuestros cálculos puede deberse a que la utilidad deje una frnaja de seguridad de dos DWORD's. Si le indicamos al Topo el número de bytes que necesitamos puede crearnos una burbuja de NOP's de la extensión solicitada, redirigir el punto de entrada hacia el inicio de dicha burbuja y finalizarla con un JMP hacia el punto de entrada original.... una gran herramienta... ;-)

¿Cuáles son nuestras necesidades?

Necesitamos por un lado espacio para almacenar las variables de nuestro código, que en su mayor parte son cadenas de caracteres... nombre de la clave del registro, librería y función a cargar,... Con unos 128 bytes debemos tener de sobra.

Por otro lado hace falta sitio para nuestro código. El código deberá cargar la dirección de la función 'RegDeleteKeyA' perteneciente a 'Advapi32.dll', realizar la llamada a dicha función para eliminar la clave deseada y saltar al punto de entrada original de la aplicación... Con 64 bytes más nos podemos apañar holgadamente...

En una aproximación muy holgada hemos calculado 192 bytes, lo cual es toda una mansión para el albergar el poco código que nos quedará al final.... No vamos a tener ningún problema ya que el ejecutable dispone de toda una suite de 364 metros cuadrados que nos permitirán hacer todas las maravillas que se nos ocurran... ;-)

AMUEBLANDO LA CASA

Nos situamos con el editor hexadecimal a partir de la zona que vamos a comenzar a amueblar. En primer lugar introduciremos los parámetros que vamos a necesitar para invocar a todas las funciones. Colocados sobre el offset 1E94h colocamos nuestras cadenas de caracteres: nombre de la función a cargar, nombre del módulo que la aloja y rama del registro a borrar. Cada cadena debe finalizar con un byte cero:

00001E90 0000 0000 6164 7661 7069 3332 0052 6567 ....advapi32.Reg
00001EA0 4465 6C65 7465 4B65 7941 0053 6F66 7477 DeleteKeyA.Softw
00001EB0 6172 655C 5642 2061 6E64 2056 4241 2050 are\VB and VBA P
00001EC0 726F 6772 616D 2053 6574 7469 6E67 735C rogram Settings\
00001ED0 446C 6C46 756E 6374 696F 6E43 616C 6C5C DllFunctionCall\
00001EE0 4465 6D6F 0000 0000 0000 0000 0000 0000 Demo............
00001EF0 0094 1E40 009D 1E40 0000 0040 00E5 1E40 ...@...@...@...@
00001F00 0000 0000 0000 0000 0000 0000 0000 0000 ................

Como puede observarse los datos insertados han sido los siguientes:

DATO
OFFSET
RVA
Nombre del módulo
1E94
401E94
Nombre de la función
1E9D
401E9D
Clave del registro
1EAB
401EAB
Tres DWORD's
1EE5
401EE5
Parámetros DllFunctionCall
1EF1
401EF1

Tal y como vimos antes, la estructura de parámetros situada en la RVA 401EF1, y que se pasarán a 'DllFunctionCall' contiene cuatro DWORD's de la forma que sigue:

DATO
Valor
Puntero al nombre del módulo
401E94
Puntero al nombre de la función
401E9D
Base de la imagen
400000
Puntero a la estructura de retorno
401EE5

Tras los datos, llega la hora de colocar el código. Para dejar algunos bytes de guarda empezaremos a codificar a partir de 1F08 (RVA 401F08). El listado fuente del código a insertar será algo así:

001 PUSH 00401EF1        ; Puntero a parámetros
002 CALL DllFunctionCall ; Carga de la función
003 PUSH 00401EAB        ; Puntero a la clave a borrar
004 PUSH 80000001        ; Identificador de HKEY_CURRENT_USER
005 CALL EAX             ; Llamada a RegDeleteKeyA
006 JMP 00401074         ; Salto al punto de entrada original

En las dos primeras líneas obtenemos la dirección de 'RegDeleteKeyA'. En las tres siguientes empilamos los parámetros de dicha función (líneas 003-004) y la llamamos mediante el registro EAX, en el que la llamada a 'DllFunctionCall' habrá cargado la dirección de 'RegDeleteKeyA'. Finalmente saltamos al punto de entrada original del programa.

Se ha omitido la comprobación del error que se puede producir en la llamada a 'DllFunctionCall' en caso de que ésta no consiga cargar la dirección de la función solicitada. Suponemos que no se produce ningún problema ya que se trata de la carga de una función perteneciente al sistema operativo... en caso de que existiera algún problema, como que la DLL 'Advapi32' no existiera o estuviera corrupta, difícilmente habría podido arrancar Windows sin acceso al registro... ;-)

Este código puede insertarse de distintas maneras... Podemos ensamblarlo en sus posiciones correspondientes con ayuda del SoftIce o el TRW2000 para después volcar el binario generado a un fichero y poderlo insertar con ayuda de un editor binario... O directamente ensamblarlo sobre el ejecutable usando el Hiew (podéis pillarlo en Protools) de Eugene Suslikov que no es más que un editor hexadecimal pero con muchos extras (como un ensamblador, visor de cabeceras PE,... una maravilla). Como última opción podemos coger la tabla de códigos de operación (opcodes) de la familia Intel 80x86 y hacerlo a pelo codificando directamente en código máquina...

Nos metemos con el Hiew para probar nuevas sensaciones... Antes tenemos que localizar hacia dónde debemos saltar para enganchar con la función 'DllFunctionCall'. Esta función es importada durante la carga del ejecutable por el sistema operativo, el cual colocará la dirección de la función en algún sitio. Este sitio es el que le indica al sistema operativo la tabla de importaciones del ejecutable, que en nuestro caso comienza en el offset 1D94h. Esta tabla enumera todas las librerías desde las que se importan funciones, las funciones que se importan de cada una y la posición en la que el sistema operativo debe almacenar la dirección de cada función importada... Cada vez que el programa desea acceder a alguna de las funciones importadas hace un 'Call' sobre un sitio fijo, en el que se encuentra un 'Jmp' hacia la dirección contenida por la posición donde el sistema operativo ha almacenado la dirección de la función. Con esto se evita que el sistema operativo, al cargar el ejecutable, tenga que modificar el programa en todos aquellos lugares en los que se realizan llamadas a las funciones importadas, así solo lo hace en una posición en la que escribe la dirección... aunque esto de las importaciones ya es tema de otro tutorial... ;-)

Si suponemos que no tenemos ni idea de cómo va la tabla de importaciones, para localizar en qué sitio se almacena la dirección de nuestra deseada 'DllFunctionCall', o mejor, dónde está ese salto que nos lleva a la función importada, podemos acudir al SoftIce (como siempre) o a un desensamblador como el W32Dasm o el IDA... Desensamblando el ejecutable nos encontramos que el citado salto está en la RVA 401030h, y que la dirección es cargada por el sistema operativo en la RVA 40100Ch.

El código final a introducir nos quedará así:

001 PUSH 00401EF1     ; Puntero a parámetros
002 CALL 00401030     ; Llamada a DllFunctionCall
003 PUSH 00401EAB     ; Puntero a la clave a borrar
004 PUSH 80000001     ; Identificador de HKEY_CURRENT_USER
005 CALL EAX          ; Llamada a RegDeleteKeyA
006 JMP 00401074      ; Salto al punto de entrada original

Desde el Hiew cargamos el ejecutable y nos desplazamos hasta el offset 1F08h (como podemos ver, una de las ventajas del Hiew es que cuando nos colocamos en offsets mapeados en memoria por la cabecera PE se suma la base de la imagen), nos ponemos en modo 'Decode' (F4), pulsamos 'Edit' (F3) y finalmente 'Asm' (F2) para empezar a ensamblar. Tened en cuenta que a la hora de introducir saltos (jmp's, call's,...) en el ensamblador del Hiew, la dirección de destino debéis indicarla restándole la base de la imagen, es decir, los saltos se hacen referidos al offset del fichero, y no a la RVA. Finalmente nos debe quedar algo así:

00401F08: 68F11E4000    PUSH 00401EF1
00401F0D: E81EF1FFFF    CALL 00401030  ; (CALL 01030)
00401F12: 68AB1E4000    PUSH 00401EAB
00401F17: 6801000080    PUSH 80000001
00401F1C: FFD0          CALL EAX
00401F1E: E951F1FFFF    JMP 00401074   ; (JMP 01074)

El paso final será cambiar el punto de entrada original (1074h) por el de nuestro código (1F08h). Esta tarea se puede realizar con un editor binario (si conocemos la estructura de las cabeceras PE y sabemos donde tocar) o con el ProcDump o similares.

Si ejecutamos el programa modificado y monitorizamos los accesos al registro con el RegMon veremos como el ejecutable, al arrancar borra la clave del registro. Posteriormente, cuando se realiza el acceso al registro desde el código en Visual Basic no encuentra la clave y vuelve a crearla... Misión cumplida.

CONCLUSIONES

En este tutorial hemos realizado la inserción de código sin modificar la longitud del fichero, lo que permitirá construir un sencillo parcheador que realice los cambios de forma automatizada.

La inserción de código nos permitirá multiplicar nuestras habilidades y posibilidades. En este caso, gracias a la inserción de código, no hemos tenido que bucear en el código de Visual Basic (lo cual es malo para la salud), ni en el infernal p-code (lo cual es de locos). Hemos solucionado el problema limpiamente sin ensuciarnos las manos... El uso de 'DllFunctionCall' en ejecutables de Visual Basic nos facilitará la inserción de código con la importación de cualquier librería o API, crear hilos que se ejecutan en paralelo con el programa de Visual Basic.... en definitiva híbridos VB+ASM... ;-)

La inserción de código permite realizar muchas otras cosas... hookear funciones importadas por el ejecutable para falsear los resultados de las mismas, o mostrar los argumentos con los que se invocan... ;-) Si a esto le unimos la posibilidad de ejecutar funciones albergadas en librerías creadas por nosotros, eliminamos los problemas de la falta de espacio para nuestro código.

Conocer los procedimientos de inserción de código nos permitirá solucionar problemas que por otro camino serían mucho más difíciles de acometer, o incluso de solución inviable...

 

Agradecimientos:

Mr.Crimson........................... Por sus "topos" y demás criaturitas... :-)
Mr.Pink y Estado+Porcino..... Escriben poco... pero cuando escriben... {:-o

 

Mr.Blue [WkT!]

[ Entrada | Documentos Genéricos | WkT! Web Site ]
[ Todo el ECD | x Tipo de Protección | x Fecha de Publicación | x Orden Alfabético ]
(c) Whiskey Kon Tekila [WkT!] - The Original Spanish Reversers.
Si necesitas contactar con
nosotros , lee esto antes e infórmate de cómo puedes ayudarnos