|
|
 |
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 direcci on
|
// 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 |
|
|
|
|
|