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

Programa Cracking  MultiTarea!: AZPR W95 / W98 / NT 
Descripción Aplicación de las capacidades multisubproceso de Windows a la desprotección de software. 
Tipo Trial con limitaciones
Tipo de Tutorial [X]Original, []Adaptación, []Aplicación, []Traducción
Url http://www.elcom.com
Protección Serial Unico por hashing
Dificultad 1) Principiante, 2) Amateur, 3) Aficionado, 4) Profesional, 5) Especialista 
Herramientas SoftIce v3.25 y UltraEdit
Objetivo Simular registro SIN USAR CARGADORES
Cracker Mr.Crimson
Grupo Whiskey Kon Tekila
Fecha 15 de Enero de 2000
 


INTRODUCCION
La mayor parte de las técnicas que se aplican usualmente a la desprotección de software se apoya en la modificación de datos o instrucciones en frío, es decir, alterando los ficheros de código o de datos de la aplicación previamente a la ejecución. 

Con la aparición de los empaquetadores / encriptadores las cosas se pusieron un poco más difíciles aunque esta estrategia seguía siendo aplicable puesto que alguna parte del código responsable del desempaquetado debía estar accesible en el fichero para poder iniciar la carga del programa (ver los ejemplos de UPX y Aspack en "Reversing with ToPo").

Sin embargo el consumo de tiempo en trazar un packer podía  llegar a hacer esta técnica poco recomendable y comenzaron a usarse  los cargadores. 
Aprovechando el "re-descubrimiento" de algunas APIs relacionadas con el depurado de procesos fue posible escribir códigos diminutos (distribuibles) capaces de cargar otros procesos y detenerse justo antes de la normal ejecución, cuando el unpacker ha terminado su trabajo y el código se encuentra expuesto y vulnerable al parcheado en caliente.

En este tutorial se plantea una alternativa al uso de cargadores basada en la posibilidad de apropiarnos de una tajada de CPU para ejecutar nuestro crack al mismo tiempo que el programa víctima esta siendo desempaquetado.

NO ME GUSTAN LOS CARGADORES
Sólo es una manía personal y no tengo argumentos muy sólidos. Simplemente pienso que debería ser la última alternativa. Si un programador nos 'obsequia' con una protección bien planteada debería responderse con un igualmente bien planteado PATCH o KEYGEN siempre que sea posible.
Si estamos de acuerdo en que lo realmente importante de la Ingeniería Inversa no es burlar a un mercader de bytes sino la posibilidad de modificar la realidad que nos rodea. También coincidiremos en que debe hacerse siguiendo las reglas no escritas del juego...

El uso de cargadores presupone el éxito de la protección o, peor aun, que ni siquiera hemos dedicado tiempo a estudiarla.
La técnica que contaré en este tute no es ninguna panacea y por el momento tiene algunas limitaciones aunque abre la via para una alternativa futura al uso de loaders.

Ok. Hasta aquí las divagaciones.

AZPR, EL ESTILO ELCOM
Esta firma se ha hecho popular por sus programas recuperadores de claves. Se trata de pequeñas aplicaciones muy compactas diseñadas para atacar por fuerza bruta ficheros protegidos con password. 
A parte de la  que nos ocupa para ZIPs , pueden encontrarse versiones para Word, Excel y  Access si no recuerdo mal. 

En la práctica, el éxito de estas aplicaciones se reduce a los casos de 'clave insegura' (p.ej. aquellas con menos de 7 caracteres). En cualquier caso, están muy optimizadas, son sencillas de usar y cumplen su función... Estas cualidades han sido reconocidas con algún que otro premio al mejor software.

En todas las aplicaciones de Elcom se respira el mismo esquema de registro basado en un número de serie fijo, igual para todos los usuarios.

Hay dos formas de atacar una comprobación de número de serie:

  1. (1) Atacar la validación. De esta forma cualquier serial podría llegar a ser válido.
  2. (2) Determinar el número de serie correcto por análisis del algoritmo de validación.
Si asumimos que el empaquetado/encriptado es robusto (al menos lo suficiente para intentar otras vías) podemos descartar la primera aproximación.
Queda la segunda y más elegante. 
FORJANDO CUCHARAS EN CASA DEL HERRERO
Cómo funciona un sistema de validación de numero secreto...?
Un esquema genérico podría ser el siguiente:
  1. El usuario introduce sus datos y un serial supuestamente válido.
  2. A partir de los datos de usuario, el código genera el serial resultante.
  3. Se compara el generado con el introducido
El punto vulnerable de esta secuencia tiene lugar en el momento en que el serial calculado se encuentra en memoria pues es visible desde debugger (Macromedia & Rsagnt32.dll).
Para reforzar este esquema basta con encriptar el proceso de manera que la operación de comparación pueda tener lugar en nuestras narices sin que nos demos cuenta. De hecho es lo más usual aunque el 'método abierto' sigue en uso en algunas protecciones comerciales (RSA Agent).

El esquema de arriba es el proceso directo porque determina la consistencia entre los datos de usuario y el serial propuesto. Se dice que el algoritmo es invertible cuando puede determinarse un serial consistente a partir de cualesquiera datos de usuario. De hecho eso es precisamente lo que hace el departamento comercial cuando decidimos registrarnos...

Ahora echemos un vistazo a AZPR. No hay petición de datos. No son tomados del sistema. Solo se pide un numero de serie que será el mismo para todos los usuarios. El esquema difiere ligeramente del anterior:

  1. El usuario introduce un serial supuestamente válido.
  2. Esta entrada se encripta convenientemente
  3. Se compara con una estructura almacenada internamente
Esta última estructura constante es la versión encriptada del serial correcto. Pero el cálculo NO tiene lugar durante el proceso de validación porque ES LA MISMA PARA TODOS. De manera que en ningún momento tenemos acceso al serial válido aunque conocemos en qué se transforma y conocemos el algoritmo de transformación.

Para acceder al serial desde aquí hace falta, que el algoritmo sea invertible y no lo es. No hay ninguna necesidad de ello...porque el serial es FIJO!. 

Mucho lío? Para aclararnos, XOR es una operación invertible porque podemos deshacer su efecto operando de nuevo:

xor (1234h, 5678h) = 444Ch
xor (444Ch, 5678h) = 1234h

OR no es invertible porque hay varias formas de obtener el mismo resultado y no se pueden recuperar los operandos sin ambigüedad:

or (1234h, 0010h) = 1234h
or (1234h,  0020h)  = 1234h

El desarrollo del algoritmo en AZPR es a grandes rasgos como sigue (*):

  • Se desechan las entradas de menos de 6 caracteres.
  • El serial introducido es fragmentado en dos pedazos.
  • Los tamaños de los trozos dependen del número total de caracteres.
  • Cada fragmento es encriptado y transformado en una estructura de 16 bytes.
  • Cada estructura es comparada con otra fija en el código.
Como se puede atacar esto...? Con fuerza bruta. No podía ser de otra manera estando en casa del herrero. Esta gente sabe bien lo fácil que es proteger usando passwords si estos se eligen bien.

Asi las cosas, escribí un código de fuerza bruta para chequear todos los posibles números de serie dentro de un rango. El algoritmo de encriptado tiene casi 900 instrucciones por lo que era necesario codificar de la foma más eficiente posible para ganar velocidad (en win32asm, por supuesto). 
Qué resulto de todo ello...?

(*) Nota: Aunque en el tiempo de escribir estas notas no me preocupe de su analisis, el algoritmo podria tratarse de MD5 o alguna variante

LA FUERZA BRUTA
Veamos algunas cifras:

El número de combinaciones diferentes para un serial depende el tamaño del serial y del número de caracteres diferentes que pueden usarse según:

Número de combinaciones = [Número de caracteres posibles]** [tamaño del serial]

Ej.(1)  Usando sólo números con un tamaño de serial de 5 caracteres

N = 10**5 = 100.000

Ej.(2)  Usando números y letras minúsculas con un tamaño de serial de 5 caracteres

N = (10 + 26) **5 = 60 millones (approx.)

Ej.(3)  Usando números, letras minúsculas y mayúsculas con un tamaño de serial de 5 

N = (10 + 26 +26 )** 5 = 916  millones (approx.)

El código de rastreo por fuerza bruta conseguía chequear sobre 800.000 serials / segundo corriendo en un PIII a 500Mhz. Cuanto tardaría en chequear "TODAS" las posibilidades con minúsculas, mayúsculas y números...?

(*)Mayúsuculas + minúsculas + numeros = 62 caracteres posibles
Número de dígitos
Tiempo
6 20 horas
7 60 días
8 9 años
9 536 años
10 33.000 años

Eso sin incluir signos como guiones, puntos, etc.
Serviría de algo trabajar en hacer el código más eficiente...? De nada. Aún incrementando su velocidad en un factor 1000 tomaría 33 años chequear todos los serial de 10 caracteres!

En versiones previas de AZPR el número de serie constaba de 26 caracteres de los cuales los 10 primeros se mantenían fijos e independientes de la versión. Si suponemos que esto continúa siendo así se reduce el cálculo al chequeo de un serial de 16 dígitos. Aún así volvemos a estar casi como antes... :o(

Para qué seguir en esta línea si estadísticamente no vamos a tener resultados antes del final del sistema solar?. Dijimos que el algoritmo no era invertible pero no nos consta que sea único. Esta claro que la clave correcta funciona pero podrían existir otras claves de menor tamaño que generen la misma secuencia...

El programa estuvo corriendo una semana ininterrumpidamente sin obtener resultados en tamaños de 6-7 dígitos con lo que decidí dar por terminada esta tentativa :o(

FALSEANDO LA CONDICION DE REGISTRO

. Recapitulando, no parece posible determinar el número de serie dado que el algoritmo no es invertible. Debemos entonces intentar simular el registro falseando la validación. 

La validación que nos interesa en este caso es la que tiene lugar al iniciarse el programa. La aparición de la funciones 'RegOpenKey' y 'RegQueryValue' durante el trazado anuncian la inminencia del chequeo pues son las responsables de extraer el posible serial del registro de Windows. 

Sin demasiada complicación pronto se encuentra el nucleo de la validación que, como puede verse es invocado unas cuantas veces (de hecho con cada operación del programa se re-autentifica el serial...!):

* Referenced by a CALL at Addresses:
|:004017A5   , :004017EE   , :00401868   , :00401AB6   , :004022C3 
|:00402D20   , :00402D6A   , :00402DA1   , :00403484   , :00406285 
|:004063C3   , :0040C4D6   , :0040C5BD   , :0040C631   , :0040C65D 
|:0040C818   , :0040C93D   , :0040E3C8   , :0040E962   , :0040EF5E 
|:0040F0C8   , :00411018   , :00411060   , :00424967 
|
:0040765D 6844000000    push 00000044
:00407662 E876A80000    call 00411EDD
:00407667 83EC40        sub esp, 00000040
:0040766A 89E0          mov eax, esp
:0040766C E800FFFFFF    call 00407571
:00407671 89E0          mov eax, esp
:00407673 E8CBFDFFFF    call 00407443 <---- eax=0 BadGuy
                                            En otro caso OK! 

:00407678 83C440        add esp, 00000040 
:0040767B C3            ret

Como siempre buscamos el MINIMO cambio posible y la mejor opción es sustituir el opcode E8 (CALL) por B8 (MOV EAX) con lo que conseguimos cambiar en 1 solo byte a:

:00407673 B8CBFDFFFF    mov eax,FFFFDCB

Con esto se obtiene el código desbloqueado, sin limitaciones y desaparece la opción de registro. Hasta aqui nada interesante. La cuestión se centra ahora en cómo sortear la 'barrera' del empaquetado para acceder a estos datos.

CONTRATAR UN ASESINO

Como todos sabemos, Windows soporta la multitarea. Por eso podemos ejecutar varios progamas a un tiempo. Mas aún, un programa puede ejecutar varios subprocesos (threads) que se ejecutan concurrentemente repartiéndose de alguna manera el tiempo de CPU.

Lo que se pretende es:

  1. Cazar el EntryPoint de AZPR 
  2. Desviarlo a una rutina que sea capaz de arrancar un subproceso. 
  3. Este subproceso correrá paralelamente al desempaquetado de AZPR chequeando si el offset  :00407673h contiene ya el valor E8h y cambiándolo entonces por B8h
  4. El subproceso muere cuando consigue su objetivo.
"Correrá paralelamente" quiere decir que una parte del tiempo la CPU estará desempaquetando AZPR y otra chequeando el offset indicado con un nivel de prioridad semejante entre ambas tareas. El rasgo de interés de esta estrategia se basa en que no importa cual sea el packer. Simplemente hay que lanzar el 'thread' asesino desde el principio con una misión y se desactivará una vez cumplida....!

Primero veremos cómo escribir este código y después como insertarlo en AZPR.

CODIFICACION DE UN SUBPROCESO

La función que crea un subproceso es:

HANDLE CreateThread(
    lpThreadAttributes, // Puntero a atributos de seguridad 
    dwStackSize,        // Tamaño de la pila de subproceso 
    lpStartAddress,     // Puntero a la función subproceso 
    lpParameter,        // Argumento para el subproceso 
    dwCreationFlags,    // Flags de creación 
    lpThreadId          // Puntero ID del subproceso creado 
   );

Para esta aplicación baste decir que puede darse a los argumentos el valor cero salvo a:

(3º) que establece cual es la rutina de subproceso, 
(5º) que es NORMAL_PRIORITY_CLASS (constante con el valor 20h)
(6º) que es una variable que contendrá el ID del subproceso.

(ver la Win32 Programmer's Reference para más información)

Esta función devuelve un handle al subproceso que no será utilizado de nuevo por lo que hay que cerrarlo de inmediato con CloseHandle para evitar pérdida de recursos. El subproceso comienza a correr desde que se ejecuta con éxito esta función.

El código entonces debe ser algo como (el offset base es arbitrario):

:00000073 683D614B00       push offset ThreadID 
:00000078 6A20             push 00000020 <-- NORMAL_PRIORITY
:0000007A 6A00             push 00000000
:0000007C 689C614B00       push 000000CE <-- Offset Thread
:00000081 6A00             push 00000000
:00000083 6A00             push 00000000
:00000085 FFD0             call CreateThread

:00000088 681C614B00       push eax      <-- Handle de Thread
:00000093 FFD6             call CloseHandle
:00000097 E964EEFEFF       jmp EntryPoint_Original_AZPR
 

Thread:
----------------------------------
:000000CE 6873764000       push 00407673  <-- offset a parchear
:000000D3 FF1539614B00     call IsBadCodePtr  <-- Esta accesible?
:000000D9 85C0             test eax, eax
:000000DB 75F1             jne 000000CE
:000000DD 803D73764000E8   cmp byte ptr [00407673], E8 <-- Es CALL?
:000000E4 75E8             jne 000000CE
:000000E6 C60573764000B8   mov byte ptr [00407673], B8 <-- MOV! :000000ED C3               ret

Véase que el thread tiene la estructura de rutina normal , es invocada de la misma manera y retorna con RET también aunque ahí termina el parecido. El uso de la función 'IsBadCodePtr' nos evita cometer un 'page fault' al intentar leer en un área de memoria no accesible. Si el offset está accesible se mira el contenido y si éste es el esperado se modifica. Eso es

LLAMAR FUNCIONES SIN TABLA DE IMPORTACION

En este punto un nuevo problema se plantea. La tabla de funciones importadas está también empaquetada con lo cual no podemos acceder a ella para codificar nuestras llamadas. Será preciso determinar las direcciones 'a pelo' y para ello usamos GetModuleHandle y GetProcAdress:

Un ejemplo:
------------------------------
kernel           db "kernel32.dll",0
Mi_Funcion       db "CreateThread",0

push, offset kernel
call GetModuleHandle ---> Devuelve en EAX un handle a kernel32.dll

push offset Mi_Funcion
push eax
call GetProcAddress ---> Devuelve en EAX la dirección de la función 
-----------------------------

Después de esto simplemente invocamos con CALL EAX para llamar a la funcion deseada y asunto resuelto.

 

INSERCION EN SECCION SOLAPADA

Vamos a analizar las posibilidades de inserción del crack en el propio AZPR. 
Descartamos la adición de apéndices que cambien el  tamaño del fichero.
Podemos usar ToPo v1.2 para habilitar espacio en las secciones existentes simplemente reciclando el zero padding. Sin embargo hay que tener la precaución de comprobar que realmente se trata de ceros de relleno para completar el alineamiento. Si el desempaquetador usara ese espacio como buffer nuestro crack podría sobreescribirse durante la operación.

Necesitamos RESERVAR unos cuantos bytes en memoria para cargar el crack y al mismo tiempo que éste se encuentre alojado en el mismo fichero. La solución para esto consiste en definir una nueva sección solapada a cualquier otra existente pero que sólo usará el espacio libre de esta. 

Confusión? Con este ejemplo seguro que quedará claro:

Supóngase un executable con 3 secciones:
 
Nombre de sección Offset (fichero) VA (memoria) VZ (Tamaño real)
.text 400h 1000h 250h
.data 800h 2000h 100h
.rsrc A00h 3000h 95h

La sección ".text" se extiende en el fichero desde el offset 400h hasta 800h pero no todo son datos o instrucciones, sólo 250h bytes . Los 1B0h restantes son ceros introducidos durante el enlazado para mantener la alineación. En virtud de ese alineamiento las secciones sólo pueden comenzar en offset múltiplos de 200h (valor típico).

Que sucede si declaramos una nueva sección de la siguiente manera?
 
.nueva 600h 4000h 200h

Pues que podremos disponer de una copia de los 1B0h bytes libres de ".text" pero cargados en memoria a partir de la dirección 4000h. De esta manera se evita el peligro de interferir con el programa en un hipotetico uso de la misma área de datos.

Aunque no es ortodoxo, nada impide mapear una misma sección en diferentes porciones de memoria sin mas que definir una nuevas  secciones solapadas.

Finalmente decidí  añadir la sección '.nueva' de manera que pudiera hacer uso del zero padding de la sección '.rsrc' inmediatamente anterior. La tabla de secciones quedó así:

El último paso consiste en redireccionar el EntryPoint a B6000h para que el código comience arrancando el crack

Si llegado este punto no teneis muy claro como se declaran las cabeceras de sección en el formato PE buscad en la red el documento "The PE File Format" by Luevelsmeyer una pequeña obra de arte. En este mismo site podeis encontrar tutes en español sobre el tema by Numit_or 

CRC

CRC es el nombre genérico que suele dar a la técnica de verificación de integridad de datos aunque no se use necesariamente  el algoritmo original del CRC. Un programa que realice una comprobación de "CRC" podrá detectar cualquier modificación de sus bytes y dejar de funcionar o simplemente no arrancar.
AZPR hace verificaciones de este tipo en varios segmentos del programa y dependiendo
de donde decidamos insertar el crack sera necesario falsear la comparación de turno...

En mi  caso tocó en:

:66F767   mov eax, dword ptr[ebp-10] <---valor correcto
:66F76A   cmp eax, dword ptr[ebp-c]  <---valor calculado
:66F7CD   jz 66F7A8                  ----> OK!

Se tratará entonces de cambiar el JZ (74h) por JMP (EBh) en :66F7CD. Y este será otro trabajo a añadir a la misión del subproceso. 

 

EL CODIGO DEL SUBPROCESO PARCHEADOR

Más arriba hemos visto un boceto del código y luego comenté que antes de los CALLs había que averiguar la direcciones usando GetProcAddress. El resultado final es un crack de alrededor de 200 bytes que puede alojarse sin problemas y que, con algunos retoques puede hacerse independiente del offset y del programa host:
 

jmp Mi_Codigo 
(saltamos sobre un bloque de bytes que
servirá para almacenar variables y constantes)
.....
4A5CCC --> "GetModuleHandle",0
4A5CC8 --> "GetProcAddress",0
4B6102 --> "kernel32.dll",0
4B6128 --> "IsBadCodePtr",0
4B610F --> "CreateThread",0
4B611C --> "CloseHandle",0
4B6135 --> DD ?
4B6139 --> DD ?
.....
.....

Mi_Codigo:
mov edi, dword ptr [004A5CCC]  EDI apunta a GetModuleHandle
mov esi, dword ptr [004A5CC8]  ESI apunta a GetProcAdress

push 004B6102                  "kernel32.dll",0
call edi                       call GetModuleHandle
mov dword ptr [004B6135], eax  Guarda handle a kernel32

push 004B6128                  "IsBadCodePtr",0
push eax                       handle a kernel32
call esi                       call GetProcAddress
mov dword ptr [004B6139], eax  Guarda la direccion de IsBadCodePtr

push 004B610F                  "CreateThread",0
push dword ptr [004B6135]      handle a kernel32 
call esi                       call GetProcAddress 

push 004B613D                  ThreadID (return) 
push 00000020                  NORMAL_PRIORITY 
push 00000000 
push 004B619C                  Dirección de subproceso
push 00000000
push 00000000
call eax                       call CreateThread
push eax                       handle de Thread creado

push 004B611C                  "CloseHandle",0
push dword ptr [004B6135]      handle a kernel32 
call esi                       call GetProcAddress

call eax                       call CloseHandle
jmp FFFEEF00                   Regresa al EntryPoint original

Thread
............................................

(Parche para CRC)
-------------------
Espera_01:
push 0066F76D                  Dirección de byte a parchear
call dword ptr [004B6139]      call IsBadCodePtr (MEM accesible?) 
test eax, eax 
jne Espera_01                  Si no, sigue intentandolo

cmp byte ptr [0066F76D], 74    Es un salto condicional?
jne Espera_01                  Si no, sigue intentandolo 
mov byte ptr [0066F76D], EB    Cambia por salto INCONDICIONAL

(Parche para Usuario Registrado)
-------------------
Espera_02:
push 00407673                  Análogamente, con el otro parcheo
call dword ptr [004B6139]
test eax, eax
jne Espera_02

cmp byte ptr [00407673], E8
jne Espera_02
mov byte ptr [00407673], B8
ret

Si estais interesados en parchear tambien la detección de SoftICE sólo teneis que añadir otro módulo de parcheo, esta vez transformando el salto incondicional a RaiseException en un ret (:00662CFB)

(Parche para detección winice)
-------------------
Espera_03:
push 00662CFB                  Análogamente, con el otro parcheo
call dword ptr [004B6139]
test eax, eax
jne Espera_03

cmp byte ptr [00662CFB], E9   (Jmp RaiseException)
jne Espera_03
mov byte ptr [00662CFB], C3   (RET!)
ret
 

He preferido listar el código ASM en lugar del binario para evitar hacer esto demasiado críptico. Para codificar estas intrucciones podeis usar  Winice directamente con la opcion ASM o escribir vuestro propio programita.

 

CONCLUSIONES

El parche resultante funciona perfectamente desbloqueando por completo el programa y debería funcionar igualmente con otras aplicaciones similares. 

No obstante, hay una observación que hacer. Si os fijais, he dado por hecho que el subproceso siempre va a llegar a tiempo a modificar los bytes necesarios. Que pasa si el proceso principal ejecuta el chequeo de CRC antes de que el Thread haga su papel...?
Pues que tendremos un aviso al respecto y la aplicación se cerrará apaciblemente.
Intentándolo una segunda vez generalmente funciona sin problemas.
He podido comprobar que esto sucede cuando se ejecuta en CPUs muy rapidas (PIII). En máquinas más lentas no he observado nunca este efecto, aunque podría darse.

Hay alguna manera de regular el tráfico de Threads para evitar este efecto...?
Por supuesto, y se basa en esas APIs que nos son tan familiares y desconocidas al mismo tiempo ( EnterCriticalSection,  LeaveCriticalSection, etc). Sirven como semáforos para detener un thread mientras otro trabaja.... pero eso es harina de otro costal y quizá tema de otro tute.

NOTA: No funcionará bajo NT sin hacer algunos arreglos no comentados aqui...

Experimentad con las ideas expuestas aquí y vereis que hay muchas puertas esperando ser abiertas en este tercer milenio...

Up The Hammer!

MrCrimson/ [WkT!2000] 

 

[ 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