| 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) Atacar la validación. De esta forma cualquier serial podría llegar a ser válido. (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: El usuario introduce sus datos y un serial supuestamente válido. A partir de los datos de usuario, el código genera el serial resultante. 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: El usuario introduce un serial supuestamente válido. Esta entrada se encripta convenientemente 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: Cazar el EntryPoint de AZPR Desviarlo a una rutina que sea capaz de arrancar un subproceso. Este subproceso correrá paralelamente al desempaquetado de AZPR chequeando si el offset :`00407673h` contiene ya el valor E8h y cambiándolo entonces por B8h. 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