Cómo descomprimir/desencriptar manualmente un ejecutable (PE). By Black Fenix.

Bienvenido a mi doceavo tutorial sobre cracking, en esta entrega vamos a tratar un tema bastante interesante: como descomprimir un ejecutable comprimido con cualquier compresor. Este tema es bastante avanzado y se recomienda un minimo conocimiento de la estructura de los ejecutables (ficheros PE) aunque he tratado de escribirlo para que los 'newbies' se enteren.

SoftIce para Windows
W32DAsm 8.93 o IDA Pro
ProcDump 32 v1.2
Editor Hexadecimal (Ultra-Edit, HView etc.)
SoftIce memory dumper, sirve cualquiera de los siguientes: SoftDump,IceDump, ADump

Nota: Puedes buscar estas tools en http://protools.cjb.net

La compresión en los archivos ejecutables a parte de reducir su tamaño se usa para evitar el  desensamblado del programa y de esta manera se consigue que no sea posible la modificación directa del código (o eso piensan los que usan compresores/encriptadores).

Bueno, ¿ Pero si no se puede modificar el código ya no podremos crackear más programas ? Pues esto es lo que creen los que usan este tipo de compresores, pero si pensamos un poco veremos que podremos obtener el archivo sin comprimir y luego podremos modificarlo sin problemas, incluso a veces podemos parchear el ejecutable comprimido añadiendo instrucciones al código (un ejemplo en mi tutorial número 11).

Tengamos en cuenta lo siguiente:

marcador.gif (1024 bytes) Para que un archivo pueda descomprimirse debe de tener una parte de su código que no este comprimida y se encarge de descomprimir la parte que si esta comprimida, bien esto es cierto pero...

marcador.gif (1024 bytes) Puede que la parte de código que descomprime el archivo, este a su vez comprimida, si esto es así, deberá existir siempre una porción de código no comprimida que se encargue de descomprimir la rutina que descomprime el resto del archivo (valga la redundancia).

Conclusión: Sea como sea para que un archivo comprimido pueda funcionar, debe descomprimirse en memoria. Esto significa que en algún momento de la ejecución del programa, este se auto descomprimirá y nosotros podremos verlo, modificarlo o copiarlo. Con lo cual podremos pasarlo de nuevo al disco desde la memoria y reconstruirlo si es necesario.

La teoria sería la siguiente:

marcador.gif (1024 bytes) 1.- Desensamblar el archivo comprimido. Esto puede resultar dificil, ya que algunos desensambladores no realizan bien esta tarea (ej. W32DAasm). Yo recomiendo usar el IDA Pro.

marcador.gif (1024 bytes) 2.- Ir al punto de entrada del programa. Y realizar un examen preliminar del código. Bucar las siguientes ocurrencias:

call [registro + offset]    -> posibles llamadas a rutinas de decodificación
call [address]           
-> o del Kernel. registro suele ser EBP
rep movsd           
-> usadas para copiar el codigo descomprimido
rep movsw
rep movsb

marcador.gif (1024 bytes) 3.- Identificar la/s llamada/s que realizan la descompresión del archivo, puede que no todo el archivo este comprimido y solo lo esten algunas secciones (ej: .code .data .idata )

Nota: Algunos compresores, comprimen tambien en otra sección el código que descomprime las secciones del archivo, por lo que no podremos identificar la/s llamada/s hasta que ejecutemos el archivo con el debugger. Normalmente esta sección se descomprime en tiempo de ejecución y se copia al código del programa, una vez copiado el código descomprimido, se salta a este código para proceder con la descompresión del resto de secciones y luego se salta al punto de entrada del archivo descomprimido.

marcador.gif (1024 bytes) 4.-Identificar las secciones que estan comprimidas y anotar sus nuevos tamaños así como su RVA (Relative Virtual Address). La RVA sería la posición de memoria donde se descomprime cada una de las secciones comprimidas.

ej. xxxxxxxx:00401000 -> Esta sería la RVA

Te recomiendo que lo escribas en un papel de la siguiente manera:

Sección:    RVA    Tamaño
--------------------------
.code 401000h 5000h
.data 406000h    1000h
.....    .....    .....

Nota:Algunos compresores modifican la tabla de importación (.idata) una vez descomprimida, por lo que deberemos asegurarnos que volquemos a disco la versión no modificada pero si descomprimida. Si no lo hacemos así no funcionará. Esto sucede con el compresor ASPack v1.08.04.

marcador.gif (1024 bytes) 5.- Buscar donde se salta al punto de entrada del archivo descomprimido. Anotar este número.

marcador.gif (1024 bytes) 6.- Una vez en el punto de entrada original, podemos tener la certeza de que todas las secciones que debian ser descomprimidas han sido descomprimidas y así volcar cada una de estas secciones a disco.

marcador.gif (1024 bytes) 7.- Ahora nos faltará volcar las secciones no comprimidas del archivo comprimido a disco y anotar sus RVA y tamaños. También nos faltará la cabezera del ejecutable, que la volcaremos a disco. La cabezera empieza en el inicio del archivo comprimido y termina en el offset de la primera sección.

marcador.gif (1024 bytes) 8.- Unir todas las secciones en un solo archivo en el mismo orden en el que aparecen en el archivo comprimido y unir al principio del resultado la cabezera.

    ej: EXE = Cabezera PE + .code + .data + .idata + .rsrc + .reloc + ...

Nota: es MUY IMPORTANTE anotar los nuevos offsets dentro del archivo (Raw Offset) de cada una de estas secciones.

marcador.gif (1024 bytes) 9.- Modificar en la cabezera del ejecutable, el punto de entrada del programa para que sea igual al del paso 5. (Punto de entrada del programa descomprimido).Esto debe ser así por que el programa ya esta descomprimido, y antes su punto de entrada era el de la rutina de descompresión, como ahora ya está descomprimido no hay que ejecutar esta rutina (si se hace, el programa no funcionará).
Modificar tambien los offsets (Raw Offset) y tamaños (Raw Size) de todas las secciones. Esto es debido a que ahora el archivo ocupará más tamaño y las secciones no estarán en los mismos puntos que antes.

Nota: El tamaño de sección sólo variará en las secciones que estuvieran comprimidas, pero el offset variará para todas menos para la primera.

marcador.gif (1024 bytes) 10.- Si todo esta bien, ya podremos ejecutar el archivo. Opcionalmente podremos eliminar las secciones que contenian las rutinas de descompresión pero esto nos obligará a modificar los offsets de las secciones que hubiera tras esta y posiblemente el punto de entrada para la tabla de importación (Import Table).

Algunos compresores pueden eliminar la sección .reloc del archivo no comprimido o variarla ya que esta parece que no es necesaria en los archivos EXE (ASPack es un ejemplo).
Tambien pueden sustituir y/o modificar la tabla de importación usada por el archivo original para que esta aparezca de manera incorrecta al desensamblar el archivo descomprimido y así dificultar el trabajo a los posibles crackers. (ASPack es un ejemplo)

Cómo saber si estamos ante un archivo comprimido ?

Podemos identificar la sección añadida por el compresor mirando los nombres de estas, algunos de estos nombres pueden ser:

.aspack
.petite
.annakin
.madmat
.WeiJunLi

Pero no te fies, siempre se puede cambiar de nombre una sección.

Puedes saber si esta comprimido si al desensamblar el archivo el código no aparece correctamente o las referencias a las DLL tampoco o las referencias a dialogos y cadenas tampoco.

Tambíen puedes usar una utilidad de identificación de jecutables, pero está no detectará los tipos de archivo que no conozca.

Cómo encontrar el punto de entrada original ?

El punto de entrada del programa descomprimido podremos localizarlo facilmente: Una vez finalizada la descompresión el programa deberá saltar a este punto, esto significa cambiar el EIP, y esto normalmente se hace con:

un salto in/concicional:
    jmp
    jnz
    je
    etc...
una llamada:
    call

el siguiente código:

    push punto_entrada   
-> empuja punto_entrada a la pila
    ret           
-> el ejecutar el ret, se saca el punto_entrada de la pila
               
-> y se cambia el EIP al valor sacado de la pila

También podremos saber que estamos en el punto de entrada cuando despues de una de las instrucciones
anteriores veamos algo como:

push ebp
mov ebp, esp

esto se suele hacer al inicio del programa para preparar la pila. Tambien sabremos que habremos pasado el punto de entrada cuando veamos llamadas a las siguientes funciones:

KERNEL32.GetCommandLine/A         -> añadir la letra A para aplicaciones de 32 bits
KERNEL32.GetStartupInfo/A
KERNEL32.GetVersion
KERNEL.INITTASK               
-> para aplicaciones de 16 bits
USER.INITAPP                
-> para aplicaciones de 16 bits
KERNEL32.GetModuleHandle/A

estas funciones se usan normalmente al inicio de las aplicaciones para prepararlas.

Cómo saber si la rutina de descompresión esta comprimida/encriptada (sistema multicapa) ?

Si el sistema de compresión utilizado comprime/encripta también la sección que descomprime/desencripta las secciones comprimidas/encriptadas, el programa deberá descomprimirla y copiarla a una zona código que posiblemente estará cercana a la zona de código que estemos trazando en ese momento. (Ojo a los cambios en la pantalla de código del SoftIce cuando estes trazando).

Cómo encontrar las rutinas de descompresión/desencriptado ?

Es lógico pensar que el programa debe descomprimir las diferentes secciones en un buffer previamente reservado, por lo que deberá alojar memoria para realizar esta operación puede que el programa use alguna de estas funciones para alojar la memoria:

LPVOID VirtualAlloc(
        LPVOID lpAddress,  
// dirección o region a reservar
        DWORD dwSize,        
// tamaño
        DWORD flAllocationType,   
// tipo de reserva
        DWORD flProtect   
// tipo de acceso de protección
    );

La función VirtualAlloc reserva o da una region de paginas en una dirección de espacio virtual del proceso que la llama. La memoria reservada por esta función es inicilizada a cero.

La función GlobalAlloc reserva el numero especificado de bytes del heap. En el entorno lineal del API de WIN32 no hay diferencia entre el heap local y el global.

HGLOBAL GlobalAlloc(
        UINT uFlags,  
// atributos
        DWORD dwBytes   
// numero de bytes a reservar
    );

Pero si la tabla de importación esta comprimida, el programa no podrá tener acceso a estas funciónes y deberá cargar antes la DLL correspondiente dinámicamente para ello podrá usar la siguiente funcion:

HMODULE GetModuleHandle(
        LPCTSTR lpModuleName    
// puntero a una cadena con el nombre del módulo
    );

La función GetModuleHandle retorna un handle de modulo para el modulo especificado si el modulo a sido mapeado en el espacio de memoria del proceso que la llama.
y luego obtener un puntero a la función deseada con

FARPROC GetProcAddress(
        HMODULE hModule,  
// handle al módulo DLL (ver GetModuleHandle)
        LPCSTR lpProcName    
// nombre de la función
    );

La función GetProcAddress retorna la dirección de una función especificada dentro de la DLL exportada. ahora si podrá usar la función, pero deberá copiar el buffer descomprimido a la posición de memoria donde se encuentre la sección, esto podrá hacerlo con la función:

VOID CopyMemory (
        PVOID Destination,   
// dirección de destino
        CONST VOID *Source,  
// dirección del bloque a copiar
        DWORD Length   
// tamaño en bytes, del bloque a copiar
    );

o con :

BOOL WriteProcessMemory(
        HANDLE hProcess,   
// handle del proceso al cual se escribe
        LPVOID lpBaseAddress,   
// dirección de inicio de escritura
        LPVOID lpBuffer,   
// puntero al buffer de origen
        DWORD nSize,       
// numero de bytes a escribir
        LPDWORD lpNumberOfBytesWritten  
// numero actual de bytes escritos
    );

La función WriteProcessMemory escribe memoria en el proceso indicado. El area a ser escrita debe ser accesible o la operación fallará. O manualmente en ensamblador con alguna de estas instrucciones o una combinación de estas.

rep movsb
rep movsw
rep movsd

Posteriormente deberá desalojar el buffer con:

BOOL VirtualFree(
        LPVOID lpAddress, 
// dirección de la región
        DWORD dwSize,       
// tamaño
        DWORD dwFreeType   
// tipo de liberación
    );

La función VirtualFree libera una region de paginas del espacio de memoria virtual del proceso que la llamó.
o con :

HGLOBAL GlobalFree(
        HGLOBAL hMem        
// handle al objeto global de memoria
    );

La función GlobalFree libera la memoria global especificada e invalida su handle.

Cómo volcar las secciones ?

Si el volcador de memoria que usemos no esta integrado con el debugger (ej. SoftDump, ADump ), y queremos volcar la sección a disco deberemos hacerlo con un volcador externo, pero si salimos del debugger el programa continuará ejecutandose y nosotros queremos que se quede en este punto mientras copiamos la sección al disco.
Para solucionar esto, podemos parar la ejecución del programa donde nos interese y en este punto ensamblar la instrucción:

jmp valor_de_eip

Nota: recuerda apuntar la instrucción que hubiera sobre esa linea.

esto dejará al programa en un bucle infinito hasta que interrunpamos de nuevo su ejecución con el debugger. Mientras el programa este en este bucle podremos ejecutar el volcador de memoria. Luego volveremos al debugger y ensamblaremos la instrucción que habia antes y continuaremos trazando si es necesario.


You are inside Reversed Minds pages.

por Mr. Silver / WKT!.
La información aquí vertida es exclusivamente para uso educacional, no puedo hacerme responsable del uso que se haga de esta, por lo que atiendo a la honradez de cada uno :), recuerda que debes comprar el software que utilices :)