Probablemente, muchas personas ya saben de primera mano qué tipo de bestia es esta:
Ghidra ("Hydra") y de qué se alimenta el programa de primera mano, aunque esta herramienta se puso a disposición del público solo recientemente, en marzo de este año. No molestaré a los lectores con una descripción de Hydra, su funcionalidad, etc. Los que están en el tema, estoy seguro, ya han estudiado todo esto, y los que aún no están en el tema, pueden hacerlo en cualquier momento, ya que ahora es fácil encontrar información detallada en Internet. Por cierto, uno de los aspectos de Hydra (el desarrollo de complementos para él) ya se ha
cubierto en Habré (¡excelente artículo!) Daré solo los enlaces principales:
Entonces, Hydra es un
desensamblador y descompilador interactivo multiplataforma gratuito con una estructura modular, con soporte para casi todas las arquitecturas principales de CPU y una interfaz gráfica flexible para trabajar con código desmontado, memoria, código recuperado (descompilado), símbolos de depuración y mucho, mucho más .
¡Intentemos romper algo con esta Hidra!
Paso 1. Encuentra y estudia la grieta
Como "víctima" encontramos un programa simple "crackme". Simplemente fui a
crackmes.one , indiqué en la búsqueda el nivel de dificultad = 2-3 ("simple" y "medio"), el idioma de origen del programa = "C / C ++" y la plataforma = "Multiplataforma", como en la captura de pantalla siguiente:

La búsqueda arrojó 2 resultados (en verde a continuación). El primer crack resultó ser de 16 bits y no comenzó en mi Win10 de 64 bits, pero
apareció el segundo (
nivel_2 por seveb ). Puedes descargarlo desde
este enlace .
Descarga y desempaqueta el crack; La contraseña para el archivo, como se indica en el sitio, es
crackmes.de . En el archivo encontramos dos directorios correspondientes a Linux y Windows. En mi máquina, voy al directorio de Windows y me encuentro con el único "ejecutable":
level_2.exe . Corramos y veamos qué quiere ella:

Parece un fastidio! Al inicio, el programa no muestra nada. Intentamos ejecutarlo nuevamente, pasándole una cadena arbitraria como parámetro (de repente, ¿está esperando una clave?), Y nuevamente nada ... Pero no se desespere. ¡Asumamos que también tenemos que encontrar los parámetros de lanzamiento como una tarea! Es hora de descubrir nuestro "cuchillo suizo" - Hydra.
Paso 2. Crear un proyecto en Hydra y análisis preliminar
Supongamos que ya tiene Hydra instalada. Si aún no, entonces todo es simple.
Instalar Ghidra1) instale
JDK versión 11 o superior (tengo
12 )
2) descargue Hydra (por ejemplo,
desde aquí ) e instálelo (al momento de escribir, la última versión de Hydra es 9.0.2, tengo 9.0.1)
Lanzamos Hydra y en el Project Manager abierto creamos inmediatamente un nuevo proyecto; Le di el nombre de
crackme3 (es decir, los proyectos crackme y crackme2 ya se han creado para mí). Un proyecto es, de hecho, un directorio de archivos, puede agregar cualquier archivo para estudiar (exe, dll, etc.). Inmediatamente agregaremos nuestro level_2.exe (
Archivo | Importar o solo la tecla
I ):

Vemos que, antes de importar, Hydra identificó nuestro graznido experimental como un PE (ejecutable portátil) de 32 bits para el sistema operativo Win32 y la plataforma x86. Después de la importación, esperamos aún más información:

Aquí, además de la profundidad de bits mencionada anteriormente, todavía podemos estar interesados en el
orden de endianness , que en nuestro caso es
Little (de bajo a alto byte), que era de esperar para la plataforma Intel 86th.
Con un análisis preliminar, hemos terminado.
Paso 3. Realizar análisis automático
Es hora de comenzar un análisis automático completo del programa en Hydra. Esto se hace haciendo doble clic en el archivo correspondiente (level_2.exe). Con una estructura modular, Hydra proporciona todas sus funciones básicas con un sistema de complemento que se puede agregar / deshabilitar o desarrollar de forma independiente. Lo mismo ocurre con el análisis: cada complemento es responsable de su tipo de análisis. Por lo tanto, primero, nos enfrentamos a esta ventana donde puede seleccionar los tipos de análisis de interés:
Ventana de configuración de análisis Para nuestros propósitos, tiene sentido dejar la configuración predeterminada y ejecutar el análisis. El análisis en sí mismo se realiza con bastante rapidez (me tomó alrededor de 7 segundos), aunque los usuarios en los foros se quejan de que para proyectos grandes, Hydra pierde velocidad con
IDA Pro . Esto puede ser cierto, pero para archivos pequeños esta diferencia no es significativa.
Entonces, el análisis está completo. Sus resultados se muestran en la ventana del Explorador de códigos:

Esta ventana es la principal para trabajar en Hydra, por lo que debe estudiarla con más cuidado.
Descripción de la interfaz del navegador de códigosLa configuración de interfaz predeterminada divide la ventana en tres partes.
En la
parte central , se encuentra la ventana principal, que enumera el desensamblador, que es más o menos similar a sus "hermanos" en IDA, OllyDbg, etc. Por defecto, las columnas en este listado son (de izquierda a derecha): dirección de memoria, código de operación del comando, comando ASM, parámetros del comando ASM, referencias cruzadas (si corresponde). Naturalmente, la visualización se puede cambiar haciendo clic en el botón en forma de muro de ladrillo en la barra de herramientas de esta ventana. Para ser sincero, nunca he visto una configuración tan flexible de la salida del desensamblador, es extremadamente conveniente.
En la
parte izquierda del panel 3:
- Secciones del programa (haga clic con el mouse para moverse por las secciones)
- Árbol de caracteres (importaciones, exportaciones, funciones, encabezados, etc.)
- Árbol de tipos de variables utilizadas
Para nosotros, la ventana más útil aquí es un árbol de símbolos, que le permite encontrar rápidamente, por ejemplo, una función por su nombre e ir a la dirección correspondiente.
En el
lado derecho hay una lista del código descompilado (en nuestro caso, en C).
Además de las ventanas predeterminadas, en el menú
Ventana puede seleccionar y colocar docenas de otras ventanas y pantallas en cualquier parte del navegador. Para mayor comodidad, agregué una ventana de Bytes y una ventana con un Gráfico de funciones en el centro, y a la derecha, variables de cadena (Cadenas) y una tabla de funciones (Funciones). Estas ventanas ahora están disponibles en pestañas separadas. Además, cualquier ventana se puede separar y hacer "flotante", colocándola y redimensionándola a su discreción; esta es también una solución muy reflexiva, en mi opinión.
Paso 4. Aprender el algoritmo del programa - función main ()
Bueno, procedamos a un análisis directo de nuestros programas crack. En la mayoría de los casos, debe comenzar buscando el punto de entrada del programa, es decir, La función principal que se llama cuando se inicia. Sabiendo que nuestro crack fue escrito en C / C ++, suponemos que el nombre de la función principal será
main () o algo así :) Se dice y se hace. Ingrese "main" en el filtro del Árbol de Símbolos (en el panel izquierdo) y vea la función
_main () en la sección
Funciones . Ve a él con un clic del mouse.
Descripción general de la función main () y cambio de nombre de funciones oscuras
En la lista del desensamblador, la sección de código correspondiente se muestra inmediatamente, y a la derecha vemos el código C descompilado de esta función. Vale la pena señalar una característica más conveniente de Hydra: la sincronización de selección: cuando un mouse selecciona un rango de comandos ASM, la sección de código correspondiente en el descompilador se resalta y viceversa. Además, si la ventana de visualización de la memoria está abierta, la asignación se sincroniza con la memoria. Como dicen, ¡todo ingenioso es simple!
Inmediatamente, noto una característica importante de trabajar en Hydra (en lugar de, por ejemplo, trabajar en IDA).
El trabajo en Hydra se centra principalmente en analizar el código descompilado . Por esta razón, los creadores de Hydra (recordamos, estamos hablando de espías de la NSA :)) prestaron gran atención a la calidad de la descompilación y la conveniencia de trabajar con código. En particular, uno puede simplemente definir funciones, variables y secciones de memoria simplemente haciendo doble clic en el código. Además, cualquier variable y función se puede renombrar de inmediato, lo cual es muy conveniente, ya que los nombres predeterminados no tienen significado y pueden ser confusos. Como verá más adelante, a menudo usaremos este mecanismo.
Entonces, aquí está la función
main () , que Hydra "diseccionó" de la siguiente manera:
Listado main ()int __cdecl _main(int _Argc,char **_Argv,char **_Env) { bool bVar1; int iVar2; char *_Dest; size_t sVar3; FILE *_File; char **ppcVar4; int local_18; ___main(); if (_Argc == 3) { bVar1 = false; _Dest = (char *)_text(0x100,1); local_18 = 0; while (local_18 < 3) { if (bVar1) { _text(_Dest,0,0x100); _text(_Dest,_Argv[local_18],0x100); break; } sVar3 = _text(_Argv[local_18]); if (((sVar3 == 2) && (((int)*_Argv[local_18] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[local_18][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } local_18 = local_18 + 1; } if ((bVar1) && (*_Dest != 0)) { _File = _text(_Dest,"rb"); if (_File == (FILE *)0x0) { _text("Failed to open file"); return 1; } ppcVar4 = _construct_key(_File); if (ppcVar4 == (char **)0x0) { _text("Nope."); _free_key((void **)0x0); } else { _text("%s%s%s%s\n",*ppcVar4 + 0x10d,*ppcVar4 + 0x219,*ppcVar4 + 0x325,*ppcVar4 + 0x431); _free_key(ppcVar4); } _text(_File); } _text(_Dest); iVar2 = 0; } else { iVar2 = 1; } return iVar2; }
Parece que todo parece normal: definiciones de variables, tipos C estándar, condiciones, bucles, llamadas a funciones. Pero observando más de cerca el código, notamos que por alguna razón los nombres de algunas funciones no fueron definidos y reemplazados por la
pseudofunción _text () (en la ventana del descompilador -
.text () ). Comencemos definiendo cuáles son estas funciones.
Hacer doble clic en el cuerpo de la primera llamada.
_Dest = (char *)_text(0x100,1);
vemos que esto es solo una función envolvente alrededor de la función
calloc () estándar, que se usa para asignar memoria para datos. Así que
cambiemos el nombre de esta función a
calloc2 () . Ajuste el cursor en el encabezado de la función, llame al menú contextual y seleccione
Cambiar nombre de función (tecla de acceso rápido -
L ) e ingrese un nuevo nombre en el campo que se abre:

Vemos que la función cambió de nombre inmediatamente. Volvemos al cuerpo
principal () (el botón
Atrás en la barra de herramientas o
Alt + <- ) y vemos que aquí en lugar del misterioso
_text () calloc2 () ya se encuentra. Genial
Hacemos lo mismo con todas las demás funciones de contenedor: vamos a sus definiciones una por una, miramos lo que hacen, cambiamos el nombre (agregué el índice 2 a los nombres estándar de las funciones C) y volvemos a la función principal.
Comprendemos el código de función main ()
Bien, descubrimos algunas funciones extrañas. Comenzamos a estudiar el código de la función principal. Saltando las declaraciones de variables, vemos que la función devuelve el valor de la variable iVar2, que es cero (un signo de éxito de la función) solo si se cumple la condición especificada por la cadena
if (_Argc == 3) { ... }
_Argc es el número de parámetros de línea de comando (argumentos) pasados a
main () . Es decir, nuestro programa "come" 2 argumentos (el primer argumento, recordamos, es siempre la ruta al archivo ejecutable).
OK, sigamos adelante. Aquí creamos una cadena C (matriz de caracteres) de 256 caracteres:
char *_Dest; _Dest = (char *)calloc2(0x100,1);
A continuación tenemos un ciclo de 3 iteraciones. En él, primero verificamos si el indicador
bVar1 está
configurado, y si es así, copiamos el siguiente argumento de línea de comando (cadena) en
_Dest :
while (i < 3) { if (bVar1) { memset2(_Dest,0,0x100); strncpy2(_Dest,_Argv[i],0x100); break; } ... }
Este indicador se establece al analizar el siguiente argumento:
n_strlen = strlen2(_Argv[i]); if (((n_strlen == 2) && (((int)*_Argv[i] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[i][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; }
La primera línea calcula la longitud de este argumento. Además, la condición verifica que la longitud del argumento debe ser 2, el penúltimo carácter == "-" y el último carácter == "f". Observe cómo el descompilador "tradujo" la extracción de caracteres de la cadena utilizando una máscara de bytes.
Los valores decimales de los números, y al mismo tiempo los caracteres ASCII correspondientes, se pueden espiar manteniendo el cursor sobre el literal hexadecimal correspondiente. El mapeo ASCII no siempre funciona (?), Por lo que recomiendo mirar la tabla ASCII en Internet. También puede convertir directamente en Hydra escalares de cualquier sistema de números a cualquier otro (a través del menú contextual -> Convertir ), en este caso este número se mostrará en todas partes en el sistema de números seleccionado (en el desensamblador y en el descompilador); pero personalmente, prefiero dejar hexes en el código para la armonía del trabajo, porque direcciones de memoria, compensaciones, etc. los hexes se establecen en todas partes.
Después del bucle viene este código:
if ((bVar1) && (*_Dest != 0)) { _File = fopen2(_Dest,"rb"); if (_File == (FILE *)0x0) { perror2("Failed to open file"); return 1; } ... }
Aquí inmediatamente agregué comentarios. Verificamos la exactitud de los argumentos ("-f path_to_file") y abrimos el archivo correspondiente (se pasó el segundo argumento, que copiamos a _Dest). El archivo se leerá en formato binario, como lo indica el parámetro "rb" de la función
fopen () . Si la lectura falla (por ejemplo, el archivo no está disponible), se muestra un mensaje de error en la secuencia de stderror y el programa sale con el código 1.
El siguiente es el más interesante:
ppcVar3 = _construct_key(_File); if (ppcVar3 == (char **)0x0) { puts2("Nope."); _free_key((void **)0x0); } else { printf2("%s%s%s%s\n",*ppcVar3 + 0x10d,*ppcVar3 + 0x219,*ppcVar3 + 0x325,*ppcVar3 + 0x431); _free_key(ppcVar3); } fclose2(_File);
El descriptor de archivo abierto (
_File ) se pasa a la función
_construct_key () , que, obviamente, realiza la verificación de la clave buscada. Esta función devuelve una matriz de bytes bidimensional (
char ** ), que se almacena en la variable
ppcVar3 . Si la matriz está vacía, se muestra el conciso "No" en la consola (es decir, en nuestra opinión, "¡No!") Y se libera la memoria. De lo contrario (si la matriz no está vacía), se muestra la clave aparentemente correcta y también se libera la memoria. Al final de la función, el descriptor de archivo se cierra, se libera memoria y
se devuelve el valor de
iVar2 .
Entonces, ahora nos damos cuenta de que necesitamos:
1) crear un archivo binario con la clave correcta;
2) pase su camino en la grieta después del argumento "-f"En la
segunda parte del artículo, analizaremos la función
_construct_key () , que, como descubrimos, es responsable de verificar la clave deseada en el archivo.