Icono del sitio Jaymon security

Reversing guiado de una aplicación de Windows


1. Introducción al ejercicio de reversing.

Para llevar a cabo este ejercicio de reversing, hemos optado por el uso de las herramientas “IDA PRO”, y “x32dbg”, ya que la aplicación a analizar fue compilada en 32 bits. Con “IDA PRO” se llevará a cabo un análisis estático del binario a analizar, mientras que con “x32dbg” se llevará a cabo el análisis dinámico. De esta manera se busca tener una mayor “conciencia situacional” (SA) con la que poder contrastar resultados y reconstruir el código fuente del binario en cuestión con mayor exatitud.

De la misma manera, se van a realizar los trabajos bajo un entorno virtualizado con Windows 7 con la intención de desactivar el Address Space Layout Randomization (ASLR) y el Data Execution Prevention (DEP), que son dos mecanismos de seguridad implementados en sistemas operativos modernos para dificultar la explotación de vulnerabilidades en el software, ya que ambos mecanismos añaden capas adicionales de protección contra ataques como desbordamientos de buffer (BoF).

2. Reconocimiento de la aplicación.

Tras desensamblar la aplicación con ambas herramientas, rápidamente nos percatamos de que se trata de una aplicación encargada de la activación de un producto. De la misma manera, se puede observar en las cadenas de caracteres aportadas en texto claro por ambos debuggers, que la clave de la licencia para la activación se encuentra sin ofuscar, en texto claro, como vemos a continuación.

Incluso, se puede encontrar la cada en texto claro al llegar a la función “strcmp” donde compara las dos cadenas de caracteres para determinar si son iguales (la introducida por el usuario, y la de la clave de licencia).

En la siguiente captura podemos observar cómo la clave de la licencia encontrada es válida.

No obstante lo anterior, el objetivo de esta práctica es profundizar en el funcionamiento y no ir directamente a la activación del producto. Es por ello que a continuación se verá cómo se lleva a cabo el funcionamiento de activación, a través de qué funciones y qué algoritmos.

3. Estudio de funciones y funcionamiento.

El código de la función “main()” se muestra a continuación. Como se puede observar, la función mas relevante es la denominada “request_license_key()”, por lo que será la primera a analizar.

El código de la función citada anteriormente es el siguiente. Se comenta de manera escueta sobre el propio código las acciones que realiza dicha función.

Para analizar paso a paso cada instrucción (F7,F8), se ha hecho uso de distintos breakpoints (F2) con el objetivo de ir controlando el flujo de la ejecución del proceso en cuestión, de manera que se pueda determinar qué es lo que se va realizando a tiempo real.

Se presenta también el código fuente de dicha función proporcionado por IDA PRO.

Así pues, se determina que se realizan las siguientes instrucciones en el orden correspondiente:

  1. mov dword ptr [esp], offset Format ; «\n>> Enter your license key to activate the platform:»: Esta instrucción coloca en la cima de la pila la dirección de la cadena presentada.
  2. call _printf: Esta instrucción llama a la función printf para imprimir la cadena mencionada anteriormente.
  3. mov dword ptr [esp+4], 64h ; ‘d’ ; : Coloca el valor hexadecimal 64h (que es 100 en decimal) en la pila, para indicar un tamaño máximo del buffer.
  4. lea eax, [ebp+Buffer] mov [esp], eax ; : Carga la dirección de un buffer llamado “Buffer” en eax y la coloca en la pila.
  5. call _fgets: Llama a la función fgets para leer una cadena desde la entrada estándar hasta el buffer hasta un máximo de 100 caracteres.
  6. lea eax, [ebp+Buffer] mov [esp], eax ; : Coloca de nuevo la dirección del buffer en la cima de la pila.
  7. call _strlen: Llama a la función strlen para calcular la longitud de la cadena que ha introducido el usuario y que se ha guardado en el buffer.

  1. sub eax, 1 mov [ebp+var_10], eax: Disminuye en uno el valor en eax (que es la longitud de la cadena) y almacena el resultado en una variable local.
  2. lea edx, [ebp+Buffer]; mov eax, [ebp+var_10]; add eax, edx; movzx eax, byte ptr [eax]; cmp al, 0Ah; : Estas instrucciones verifican si el último carácter de la cadena es un salto de línea (0Ah en hexadecimal).
  3. jnz short loc_40150C: Si el último carácter no es un salto de línea, salta a 40150C para continuar con la ejecución.
  4. lea edx, [ebp+Buffer]; mov eax, [ebp+var_10]; add eax, edx; mov byte ptr [eax], 0; : Si el último carácter es un salto de línea, lo reemplaza con un carácter nulo, terminando así la cadena.

La función “is_license_key” verifica si la cadena de caracteres ingresada (la clave de licencia) es válida o no. A continuación, se muestra el código de dicha función.

Comentamos brevemente qué es lo que sucede en dicha función:

  1. push ebp y mov ebp, esp: Son instrucciones estándar para establecer el marco de pila de la función.
  2. sub esp, 28h: Reserva 28h (40 en decimal) bytes en la pila para variables locales.
  3. mov eax, [ebp+Str]: Carga la dirección del argumento Str (la cadena de la clave de licencia proporcionada) en el registro eax.
  4. mov [esp], eax: Coloca la dirección de la cadena en la pila, preparándose para llamar a una función.
  5. call _strlen: Llama a la función “strlen”, que devuelve la longitud de la cadena proporcionada.
  6. mov [ebp+var_C], eax: Almacena la longitud de la cadena en una variable local (var_C).
  7. cmp [ebp+var_C], 24h: Compara la longitud de la cadena con el valor 24 (hexadecimal 24h es 36 en decimal). De esta manera comprueba si la longitud de la clave de licencia es exactamente de 36 caracteres, lo cual es cierto sabiendo que la clave es la siguiente:

35363FC4-8671-4F2C-AE70-4BC9045EC6A3

  1. jz short loc_40144B: Si la longitud es 36, salta a “loc_40144B”. Se expone código que analizaremos en el siguiente punto.

En caso de que la longitud de la cadena no sea 36, salta a “locret_4014A2”, es decir, a la función is_license_key(char *Str), indicando que la clave es inválida. En la siguiente imagen vemos cómo al introducir una clave de 36 caracteres continua su flujo correctamente.

Tras comprobar que la cadena presenta 36 caracteres, el flujo de ejecución continua con la función “is_master()”.

A continuación, se trata de explicar las instrucciones de esta función en su orden correspondiente:

  1. push ebp y mov ebp, esp: Son instrucciones estándar para establecer el marco de pila de la función.
  2. sub esp, 48h: Reserva 48h (72 en decimal) bytes en la pila para variables locales.
  3. mov eax, [ebp+Source]: Carga la dirección de la cadena Source (la cadena proporcionada como argumento) en el registro eax.
  4. lea eax, [ebp+Destination]: Carga la dirección de la variable local “Destination” en el registro eax. Se trata del buffer donde se almacenará una copia de la cadena “Source”.
  5. call _strcpy: Llama a la función “strcpy” para copiar la cadena “Source” a “Destination”.
  6. mov dword ptr [esp+4], offset Str2: Establece el segundo argumento para la función “strcmp”, que es la dirección de la cadena fija «35363FC4-8671-4F2C-AE70-4BC9045EC6A3».
  7. lea eax, [ebp+Destination]: Carga la dirección de la variable local “Destination” (que ahora contiene una copia de “Source”) en el registro eax.
  8. call _strcmp: Llama a la función “strcmp” para comparar “Destination” (la copia de “Source”) con la cadena fija. Esta función devolverá “0” si las dos cadenas son idénticas.
  9. test eax, eax y jnz short loc_401386: Verifica el valor devuelto por “strcmp”. Si no es 0 (lo que significa que las cadenas no son idénticas), salta a “loc_401386”, donde establecerá eax a 0.

  1. mov eax, 1: Si las cadenas son idénticas (porque no saltó en el paso anterior), establece eax a 1, y procederá a reportar el mensaje de éxito. En caso de que la cadena introducida por el usuario sea numérica, procederá a ingresar en la función “sum_numbers”.

A continuación, se muestra captura del pseudocódigo generado por el reconstructor de código de IDA PRO (F5) sobre la función “sum_numbers(char *Source)”, y su código en ensamblador.

La función “sum_numbers” toma como argumento un puntero a la cadena introducida por el usuario “(char *Source)” y devuelve un entero.

Su funcionamiento es el siguiente:

Básicamente podríamos decir que la función suma todos los dígitos numéricos que se encuentran en la cadena dada y devuelve el total. Por lo tanto, como vemos en la siguiente imagen, en la cadena introducida “1aaaaaaaaa2bbbbbbbbb3ccccccccc4ddddd” la función devolverá Ah, equivalente a 10 en decimal, porque 1 + 2 + 3 + 4 = 10.

Para concluir, podemos afirmar que el funcionamiento general de la aplicación se resume como sigue:

  1. Se imprime el mensaje para que el usuario ingrese la clave de licencia.
  2. Se almacena la entrada del usuario en un buffer.
  3. Se verifica si la última entrada del buffer es un salto de línea, y en caso de ser así se reemplaza con un carácter nulo para terminar la cadena.
  4. Se verifica la clave de licencia ingresada llamando a la función “is_license_key”.
  5. Si la clave es válida:
    • Se imprimen mensajes indicando que la clave es válida, que la activación está en progreso y que queda confirmado que la plataforma ha sido activada.
  6. Si la clave no es válida:
    • Se imprime un mensaje de error mostrando cuántos intentos de activación quedan.
    • Se incrementa el contador de intentos.
    • Se verifica si el contador de intentos es menor o igual al número máximo de intentos permitidos, y si es así, se reinicia el bucle solicitando al usuario que ingrese nuevamente la clave.
  7. Si se han superado todos los intentos permitidos, la función regresa un valor de 0 y termina.

4. Identificación de constantes literales.

Determinamos que en este ejercicio las constantes literales son las que se muestran en la siguiente captura:

En el caso de esta aplicación se constituye un error tremendo de programación al utilizar una constante literal para contener información sensible, como es el caso de la clave de licencia.

En cuanto al uso de las cadenas de caracteres que se observan en la captura anterior, tienen por objetivo el de comunicar al usuario el estado de la activación del producto, y las oportunidades que tiene este de volver a intentar el introducir una clave válida en caso de que haya fallado el intento anterior, principalmente.

5. Identificación de bucles y su funcionamiento.

En la aplicación se encuentran dos bucles y ambos son de tipo “for”.

A continuación, se muestra captura del pseudocódigo generado por el reconstructor de código de IDA PRO (F5) sobre la función “request_license_key(int a1)”.

Como se puede observar, hay un bucle en la función anterior, y es del tipo “for”. En cuanto a la descripción del bucle, se determina que:

Los condicionantes para salir del bucle son los siguientes:

A continuación, se muestra captura del pseudocódigo generado por el reconstructor de código de IDA PRO (F5) sobre la función “sum_numbers(char *Source)”.

Como se puede observar, hay un bucle en la función anterior, y también es del tipo “for”. En cuanto a la descripción del bucle, se determina que:

6. Identificación de bucle anidado y su funcionamiento.

Como se observa a continuación, se puede determinar que existe un bucle anidado, ya que se define como un bucle anidado aquel que ocurre cuando hay un bucle dentro de otro bucle.

Para ser más exacto en la respuesta:

Se adjuntan capturas de las funciones analizadas:

7. Métodos o funciones propias de la aplicación.

Las funciones propias de la aplicación son las siguientes.

No obstante, para su correcto funcionamiento tiene que hacer uso de muchas otras que no son propiamente de la aplicación, sino de librerías dinámicas del propio sistema operativo (DLLs), como vemos a continuación, pertenecientes a las librerías “kernel32.dll” y a “msvcrt.dll”.

Para no hacer muy extenso el ejercicio, decir que ya se han analizado anteriormente los códigos fuente de las distintas funciones, entre las que destacan “request_license_key()” y “is_license_key”. En cuanto a las “funciones librería” se explican brevemente aquellas más relevantes para el funcionamiento de la aplicación:

8. Identificando puntos vulnerables de la aplicación.

Analizando la aplicación, se pueden identificar algunas áreas que podrían ser potencialmente vulnerables o que, al menos, podrían mejorar en términos de seguridad:

Si hacemos un pequeño estudio, se comprueba que con un total de 197 caracteres encuentra su límite funcional. En la siguiente captura podemos ver cómo con 198 caracteres la aplicación finaliza, no dando opción a nuevos intentos de introducir la clave.

Una vez alcanzada la instrucción “ret”, realiza la llamada a la salida de la ejecución del proceso.

Como se puede observar, la función “fgets” se utiliza para leer hasta 100 caracteres (incluido el carácter de nueva línea) en el buffer “Buffer”. Así pues, la función “fgets” leerá caracteres hasta que:

Entonces, si un usuario introduce 198 caracteres sin un carácter de nueva línea, “fgets” leerá los primeros 99 caracteres en la primera iteración del bucle. En la segunda iteración, leerá los siguientes 99 caracteres. Es importante saber que “fgets” no discrimina entre diferentes líneas de entrada, pues simplemente lee caracteres hasta que se encuentre con un límite.

Entonces, después de dos iteraciones del bucle, se han leído 198 caracteres en total. En la tercera iteración, si no hay más entrada (o simplemente un carácter de nueva línea), “fgets” considerará que es otra entrada de licencia. Si no se introduce nada y simplemente se presiona «Enter», “fgets” leerá solo el carácter de nueva línea.

Como resultado, al introducir 198 caracteres seguidos de un carácter de nueva línea, se considera que el usuario ha realizado tres intentos para introducir la clave de licencia. Por eso es importante tener precaución y comprensión completa de cómo las funciones de entrada/salida estándar operan para evitar confusiones o posibles errores en el comportamiento del programa.

9. ¿Se hacen uso de funciones vulnerables del tipo strcpy, strcmp, etc.?

Efectivamente, el código utiliza dos funciones que son comúnmente reconocidas por ser potencialmente vulnerables si no se usan correctamente: “strcpy” y “strcmp”.

Como se observa a continuación, están presentes en distinas funciones de la aplicación.

Analizando el código se observa una vulnerabilidad clásica de desbordamiento de búfer en la función “is_master”. Específicamente, se utiliza la función “strcpy” para copiar una cadena de caracteres desde el parámetro “Source” hacia el arreglo “Destination”, que tiene un tamaño fijo de 44 bytes. Si “Source” contiene más de 43 caracteres (44 – 1 para el carácter nulo), ocurrirá un desbordamiento de búfer (BoF).

Para explotar este desbordamiento de búfer y causar un crash en la aplicación, basta con proporcionar una clave de licencia que tenga más de 43 caracteres. Dado que “is_license_key” requiere que la longitud sea exactamente de 36 caracteres para que “is_master” se ejecute, el desbordamiento no es directamente explotable con solo la clave de licencia. Sin embargo, si se tuviera acceso directo a la función “is_master” (o si la lógica del programa cambia en el futuro), se podría usar un payload como:

“AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA”,

para causar el crashseo de la aplicación, que bien puede radicar en una denegación de servicio (DOS), en una ejecución de código remoto (RCE), entre otros.

A continuación, vamos a introducir el payload anterior de 50 caracteres, y vamos a ir redirigiendo/modificando el flujo del proceso para que llegue a la función “is_master” y comprobar que, efectivamente, se realiza un BoF.

Como se observa en la captura anterior, se realiza un Buffer Overflow con el payload introducido, tras haber modificado el flujo de ejecución para forzar a la aplicación alcanzar la función “is_master”.

Llegados a este punto, un actor maligno podría elaborar una versión alterada del software, ajustando los códigos de operación necesarios para que el programa siempre acceda a la función comprometida. Esto permitiría que se valide como legítima una clave específica diseñada como payload, explotando el desbordamiento de búfer para conseguir una ejecución remota de código (RCE) u otros fines. Esta nueva versión “maligna” la podría subir a un repositorio desde la que poder descargarla y distribuirla como “aplicación medicinada o crackeada”, pudiendo “infectar” de esta manera a aquellos que se la descarguen, y evadiendo incluso antivirus y demás sistemas perimetrales de seguridad, dado que la aplicación en sí misma no contiene código ni comportamiento dañino. Es por todo lo anterior que no debe descargarse software pirata, ni descargarse software de repositorios que no sean oficiales o de alta fiabilidad.

Obviamente, no lo vamos a llevar a cabo porque no es el propósito de este ejercicio, pero creo conveniente mencionarlo dado que deja de manifiesto la importancia de la seguridad del software en campañas de cibercrimen.

Con la intención de mitigar la vulnerabilidad encontrada, se recomienda:

10. ¿Qué condicionales se han encontrado? ¿Qué condición se realiza? ¿Qué sucede en cada caso?

Se han identificado los siguientes condicionales:

11. Variables empleadas.

A continuación, se muestran las capturas de cada función de la aplicación, explicando brevemente las variables de cada una de ellas.

12. Muestra el uso del stack (pila) en algún punto de la ejecución.

En la siguiente imagen se puede observar el uso de la pila tras finalizar la ejecución la función “fgets”. Así pues, se puede ver que se guarda en la pila los distintos valores en hexadecimal de cada carácter introducido por el usuario (a=61h, b=62h, c=63h, etc.).

Como es obvio, estos valores serán utilizados más adelante para comprobar si la clave de licencia introducida es la correcta, mediante la cantidad de caracteres que hay (debe haber 36 bien calculados con “strleen”) y mediante la comparación directa en ASCII con la cadena de caracteres que corresponde a la clave de licencia legítima (con “strcmp”).

A continuación, se puede ver cómo se usan las instrucciones “push ebp” y “mov ebp, esp”, que son instrucciones estándar para establecer el marco de pila de una función. La instrucción “sub esp, 28h” Reserva 48h (40 en decimal) bytes en la pila para variables locales.

Concretamente, se emplea la instrucción “push ebp” para guardar en el stack (la pila) el valor del registro EBP, el cual corresponde a la clave de licencia en ASCII introducida por el usuario. Además, como ya se ha comentado, podemos ver más abajo que tras pasar la instrucción “fgets”, se guardaron en la pila los valores de cada carácter introducido por el usuario, en hexadecimal (a=61h, b=62h, etc.).

También se puede ver el uso de la pila en la función “sum_numbers”, donde se usa la pila para ir recorriendo la cadena de caracteres introducida por el usuario en busca de números para luego sumarlos.

Además de lo anterior, en el punto 7 hemos podido observar cómo se comporta la pila ante una vulnerabilidad de Buffer Overflow (BoF).

13. ¿Se puede modificar el flujo de ejecución?

Como se verá a continuación, la respuesta corta es: Sí.

El punto exacto donde se cumple la condición de éxito o de error se encuentra en la instrucción “JE program.40154B”. En este caso, para poder realizar un bypass de dicha condición y activar con éxito la aplicación, se procederá a cambiar la instrucción “JE” por “JNE”, o, lo que es lo mismo, cambiar el opcode “74” por el “75”.

Como se muestra a continuación, en el IDA en lugar de poner la instrucción JE aparece la instrucción JZ, lo cual son simplemente nombres diferentes para representar exactamente lo mismo, ya que ambos indican un salto condicional cuando ZF (la «bandera de cero») es igual a 1.

En la siguiente captura se muestra cómo ya se ha cambiado el opcode “74” por el “75” resultando el cambio de instrucción de “JZ” a “JNZ”.

Así pues, es el momento de comprobar que se ha “medicinado” el programa adecuadamente, y que de ahora en adelante aceptará cualquier tipo de licencia.

De esta manera se ha demostrado que, efectivamente, se puede alterar el flujo del código de la aplicación.

14. Otros recursos similares.

Salir de la versión móvil